Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

tcalc - a time calculator



Hi all,

I have written a time calculator (attached) that works quite like zcalc but handles times:

  % 1 + 2 * 3
  7

  % 1:0 / 3
  0:0:20.000

  % 1:0 ** 2 / 2:0
  0:0:30.000

If the code is correct and there is interest, someone (possibly me) could try to integrate its features into zcalc. (tcalc does not have configurable output formats, previous results reference, etc.)

The full code with tests is here: http://fccode.free.fr/tcalc/tcalc-0.tgz.

Best,

Florent
#! /bin/zsh -i

function help {
  cat <<EOF
tcalc is a zsh \`time calculator': it understands and displays times in
hh:mm:ss.ddd format (when the dimension of the expression is a time).

Example session:
  % 1 + 2 * 3
  7
  % 1:0 / 3
  0:0:20.000
  % 1:0 ** 2 / 2:0
  0:0:30.000
EOF
}

function warn {
  print $@ >&2
}

function is_time {
  [[ $1 == *:* ]]
}

function maybe_to_s {
  if is_time $1; then
    to_s $1
  else
    print -- $1
  fi
}

# to_s(t) prints the time t converted to seconds.
# t must be of the form [-][h...:][m]m:[s]s[.[d][d][d]].
function to_s {
  local s="$1"
  [[ "$s" == *.* ]] || s="$s.0" # make sure times are converted to floats
  local w
  w=( ${(s/:/)s} )
  if [ $#w -eq 2 ]; then
    print -- $(( $w[1] * 60 + $w[2] ))
  elif [ $#w -eq 3 ]; then
    print -- $(( $w[1] * 60 * 60 + $w[2] * 60 + $w[3] ))
  else
    warn "Failed to parse $1"
    print 0
  fi
}

# Prints the time in [-][h...]h:[m]m:[s]s.ddd format.
function print_time {
  local t=$1
  if (( t < 0 )); then
    print -n -- -
    (( t = -t ))
  fi
  (( dec_part = t - floor(t) ))
  integer t # floor
  d=$(( t / 3600 ))
  print -n "$d:"
  t=$(( t % 3600 ))
  d=$(( t / 60 ))
  print -n "$d:"
  t=$(( t % 60 ))
  print -- $t.${${dec_part#0.}[1,3]}
}

# print_res(x, dim) prints x (interpreted as a number of seconds) in time
# format if dim equals 1, as is otherwise.
function print_res {
  if [[ $2 -eq 1 ]]; then
    print_time "$1"
  else
    print -- "$1"
  fi
}

function skip_init_spaces {
  setopt re_match_pcre
  [[ $1 =~ "^ *" ]]
  print -- ${1[MEND+1,$#1]}
}

# matching_paren_pos(s) returns the index of the parenthesis that closes the
# opening one s starts with.
function matching_paren_pos {
  local i k=0 # number of open parenthesis so far
  for i in {1..$#}; do
    if [[ $argv[i] == '(' ]]; then
      (( k++ ))
    elif [[ $argv[i] == ')' && $k -eq 1 ]]; then
      print $i
      return
    elif [[ $argv[i] == ')' ]]; then
      (( k-- ))
    fi
  done
}

# token(s, pat) prints the first characters of s that match pat, followed by
# the rest of s.
function token {
  local tok="" rest="$1" pat="$2"
  while [[ "$rest[1]" =~ "$pat" ]]; do
    tok="${tok}$rest[1]"
    rest="${rest#?}"
  done
  print -- $tok $rest
}

# lex(s) prints the lexemes of s followed by a space.
function lex {
  local s="$1" tok
  local digit_re="[0-9.:]"
  local op_re="[-+*/]"
  while [ "$s" != "" ]; do
    s=$( skip_init_spaces "$s" )
    local first_char="$s[1]"
    if [[ "$first_char" =~ '\(|\)' ]]; then
      tok="$first_char"
      s="${s#?}"
    elif [[ "$first_char" =~ $digit_re ]]; then
      token "$s" $digit_re | read tok s
    elif [[ "$first_char" =~ $op_re ]]; then
      token "$s" $op_re | read tok s
    else
      tok=""
    fi
    if [[ "$tok" == "" ]]; then
      warn "Illegal character: $first_char"
      return 1
    fi
    print -n -- "$sep$tok"
    local sep=" "
  done
}

# Computes the dimension of the input expression: returns the power of time (0
# indicates it is dimension-less). Expects a valid expression.
# Roughly transforms expressions this way:
#   -1:0 ** 2 * (2 - 3) / 2:0
# becomes
#   1 * 2 + (0, 0) - 1
# To reject operations that do not make sense dimensionwise, we would need an
# infix operator that would check the dimension is the same on both sides of
# additions and subtractions. We use ',' instead, assuming the dimension is the
# same.
function time_dimension {
  local i expr
  expr=()
  # whether we are at the start of the whole expression or after an opening
  # parenthesis (to detect unary '-')
  local expr_beg=1
  i=1
  while (( i <= $#argv )); do
    case $argv[i] in
      (\()   expr[$#expr+1]=$argv[i]; expr_beg=1;;
      (\))   expr[$#expr+1]=$argv[i];;
      (\*\*) expr[$#expr+1]='*'
             [[ "$argv[i+1]" == '(' ]] &&
               last=$(( i + $( matching_paren_pos $argv[i+1,-1] ) )) ||
               last=$(( i + 1 ))
             expr[$#expr+1]=$( eval_expr $argv[i+1,last] )
             i=$last;;
      (\*)   expr[$#expr+1]='+';;
      (/)    expr[$#expr+1]='-';;
      (+)    expr[$#expr+1]=',';;
      (-)    (( expr_beg == 1 )) && expr[$#expr+1]='' || expr[$#expr+1]=',';;
      (*)    expr_beg=0
             is_time $argv[i] && expr[$#expr+1]=1 || expr[$#expr+1]=0;;
    esac
    (( i++ ))
  done
  print $(( $expr ))
}

function eval_expr {
  (( $# > 0 )) || return 1
  local i tr_words
  tr_words=()
  for i in "$@"; do
    tr_words[$#tr_words+1]=$( maybe_to_s $i )
  done
  # eval, otherwise an error in the expression exits the program
  eval 'print $(( tr_words ))'
}

function read_line { vared -chep '%% ' $1; }
while getopts "ht" swt; do
  case $swt in
    (h) help; return 0;;
    (t) function read_line { read $1; };;
    (*) help; return 2;;
  esac
done
if (( $# >= $OPTIND )); then
  print "Unrecognised arguments: $@[OPTIND,-1]" >&2
  help
  return 2
fi

zmodload zsh/mathfunc

history -ap "${ZDOTDIR:-$HOME}/.tcalc_history"
while read_line l; do
  print -s -- "$l"
  words=$( lex $l ) || continue
  res=$( eval_expr $=words ) || continue
  dim=$( time_dimension $=words )
  print_res $res $dim
  l=
done


Messages sorted by: Reverse Date, Date, Thread, Author