From:	SMTP%"jw@peaceful.org" 20-FEB-1995 12:10:43.76
To:	USRC
CC:	
Subj:	v47i066: hours - Keep track of hours worked, v1.14, Part01/01

Resent-Date: 18 Feb 1995 20:05:08 -0600
Path: decwrl!amd!amdahl.com!pacbell.com!ames!hookup!newshost.marcam.com!charnel.ecst.csuchico.edu!olivea!uunet!sparky!not-for-mail
From: jw@peaceful.org (John Wiegley)
Newsgroups: comp.sources.misc
Subject: v47i066: hours - Keep track of hours worked, v1.14, Part01/01
Followup-To: comp.sources.d
Date: 18 Feb 1995 20:05:08 -0600
Organization: Earthlink Network, Inc.
Lines: 1251
Sender: kent@sparky.sterling.com
Approved: kent@sparky.sterling.com
Message-Id: <3i690k$1ec@sparky.sterling.com>
Nntp-Posting-Host: sparky.sterling.com
X-Md4-Signature: e8e3105092386ade4c72b4f786002309
To: unix-sources@pa.dec.com
Resent-Message-Id: <"zUATN2.0.zv4.uQyHl"@ftp-gw-1.pa.dec.com>
Resent-From: unix-sources@pa.dec.com
X-Mailing-List: <unix-sources@pa.dec.com> archive/latest/491
X-Loop: unix-sources@pa.dec.com
Precedence: list
Resent-Sender: unix-sources-request@pa.dec.com

Submitted-by: jw@peaceful.org (John Wiegley)
Posting-number: Volume 47, Issue 66
Archive-name: hours/part01
Environment: Perl
Supersedes: hours: Volume 47, Issue 37

This is the "hours" timecard management program, version 1.14.  It fixes
a large number of bugs from 1.0, and also adds several new features.  It
requires perl 4.036 or higher.

"Hours" can also be used to track time-intervals of any kind.  I
personally use it both for timecard tracking, and keeping track of how
many hours my pay-by-the-hour PPP connection has been up this month.

At its simplest, type "ti" to check-in, and "to" to check-out.  Typing
"hours" will produce a report for the current month.

"Hours" has been in steady use at my company for over 5 months now.  We
find it invaluable, since it obviates the need to worry about timecards
and clock-watching!  Simply use "ti" and "to", and the end of your pay
period, generate a report and fill out your timecard based on the
results.  Painless, easy time-tracking, down to the second.

Send bug reports and comments to:
   John Wiegley
   johnw@whiplash.pc.cc.cmu.edu

#! /bin/sh
# This is a shell archive.  Remove anything before this line, then feed it
# into a shell via "sh file" or similar.  To overwrite existing files,
# type "sh file -c".
# Contents:  README Makefile NEWS hours hours.1 left tann ti tmark to
#   ts txtra
# Wrapped by kent@sparky on Sat Feb 18 20:02:22 1995
PATH=/bin:/usr/bin:/usr/ucb:/usr/local/bin:/usr/lbin:$PATH ; export PATH
echo If this archive is complete, you will see the following message:
echo '          "shar: End of archive 1 (of 1)."'
if test -f 'README' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'README'\"
else
  echo shar: Extracting \"'README'\" \(1722 characters\)
  sed "s/^X//" >'README' <<'END_OF_FILE'
XHours 1.14
X----------
X
XThis is the "hours" timecard program.  To use it, type "make install".
XIt requires perl 4.036 or greater.
X
XAfter installation, seven basic commands are available:
X
X1.  To check in, type:
X
X	ti
X
X2.  To check out, type:
X
X	to [category] ["description of what was done"]
X
X    The square brackets indicate that both of these fields are
X    optional.  Here is a sample usage:
X
X	to my_project "Finally got the program to work."
X        to my_project
X
X3.  If your day is too short, and you want to attach an explanation
X    (like visiting the doctor), you can "annotate" your last check out
X    by typing:
X
X	tann "Went to see the doctor."
X
X4.  If you want to see how many hours you have left until you can go
X    home, type:
X
X	left
X
X5.  If you go home and start doing "extra" work, check in with "ti",
X    but check out with:
X
X	txtra personal_work "This is stuff for me at home."
X
X    This will keep track of your hours, but without adding them to
X    your daily total (these are shown in the report as if they were
X    hours outside your regular work).
X
X6.  When you're ready to see what your timecard looks like, type:
X
X	hours
X
X    To see every entry (instead of just the current period's hours),
X    type:
X
X	hours -a
X
X    Specifying a category will restrict the report to hours pertaining
X    to that category:
X
X	hours my_project
X
X7.  And last but not least, you can set a "mark" by typing:
X
X        tmark
X
X    A mark in the timecard causes "hours" to report back to the last
X    mark, instead of displaying the current month (which is the
X    default).
X
X
XThat's all there is to it!  "hours -h" displays all the options
Xcurrently available.
X
X
XSuggestions are welcome,
X
X  John Wiegley
X  johnw@peaceful.org
END_OF_FILE
  if test 1722 -ne `wc -c <'README'`; then
    echo shar: \"'README'\" unpacked with wrong size!
  fi
  # end of 'README'
fi
if test -f 'Makefile' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'Makefile'\"
else
  echo shar: Extracting \"'Makefile'\" \(248 characters\)
  sed "s/^X//" >'Makefile' <<'END_OF_FILE'
XPUBDIR = /usr/local
XBINDIR = $(PUBDIR)/bin
XMANDIR = $(PUBDIR)/man
X
XTARGETS = hours ti to ts tann tmark txtra left
X
Xinstall:
X	sh -c "for i in $(TARGETS); do cp \$$i $(BINDIR); done"
X	(cd $(BINDIR); chmod 755 $(TARGETS))
X	cp ./hours.1 $(MANDIR)/man1
END_OF_FILE
  if test 248 -ne `wc -c <'Makefile'`; then
    echo shar: \"'Makefile'\" unpacked with wrong size!
  fi
  # end of 'Makefile'
fi
if test -f 'NEWS' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'NEWS'\"
else
  echo shar: Extracting \"'NEWS'\" \(2795 characters\)
  sed "s/^X//" >'NEWS' <<'END_OF_FILE'
XNew in 1.14
X===========
X
X* The options handling system was completely overhauled.  Options are
X  now order-indepedent, and many have validation checking.  You can also
X  "munge" options together, as long as the arguments appear in the
X  corresponding order after the switch (similar to 'tar').
X
X* Much of the source was redone.  There was only a 33% decrease in
X  speed for all of the added features.  The code is more modular now,
X  and more robust.  Time is tracked in flexible "indices", allowing
X  scalable reports without any effort (see next item).  This was done
X  to smooth the way for smart "nominal" periods (coming in 2.0).
X
X* You can now specify a 'base' (units to report in), a 'range' (report
X  in 'base units per range'), and a 'report range'.  For example, if
X  base is X, range is Y, and report range is Z, you can ask questions
X  of the form "show me all X per Y in the current Z".  Default is
X  hours per day in the current month.  Use of 'marking' disables Z,
X  but not X or Y.
X
X* Multiple categories can be specified by A/B/C.
X
X* The period category and comment can be associated either with a
X  check-in, or check-out now (as opposed to only with a check-out, as
X  before).  The state information is kept in the lockfile.
X
X* 'relative' mode has no meaning for category totals (before it used
X  to mean "relative to the nominal multiplied by the number of days
X  seen").
X
X* 'percentages' mode has a different meaning for category totals.  It
X  is the percentage that category contributes to the grand total.
X  Before it used to be a percentage of that category against the
X  nominal multiplied by the number of days seen.  That kind of number
X  was just about useless.
X
X* Added a 'ts' script for "switching" directly from one task to
X  another.
X
X* Six new options:
X    -d    print the number of hours worked since last check-in
X          ('d' for 'delta')
X    -R #  will round all values to the nearest #
X    -P #  will report with a decimal precision of #
X    -n #  set nominal time to # (default is 8.0)
X    -r    display report in numbers relative to nominal (per day)
X    -p    display report as percentages
X
X* 'Marking' is now supported.  If you don't use it ("tmark"), the
X  default report will always print the current month.  If marking is
X  enabled, however, reporting will show all activity since the last
X  mark, no matter how far back that mark was.
X
X* Changed format of dates in the report output.  Added more whitespace.
X
X* New checkout feature: If you've been doing two projects at once, you
X  can checkout with categories named "a/b/c".  This will apply the hours
X  worked to "a", "b" and "c" in the sub-totals (but will not affect the
X  grand total).
X
X----------------------------------
XVERSIONS before 1.14 have no NEWS.
X----------------------------------
END_OF_FILE
  if test 2795 -ne `wc -c <'NEWS'`; then
    echo shar: \"'NEWS'\" unpacked with wrong size!
  fi
  # end of 'NEWS'
fi
if test -f 'hours' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'hours'\"
else
  echo shar: Extracting \"'hours'\" \(14035 characters\)
  sed "s/^X//" >'hours' <<'END_OF_FILE'
X#!/usr/local/bin/perl
X#
X# by John Wiegley <johnw@peaceful.org>
X#
X# This program provides a simple way to report on time intervals.
X
X###
X### Declare globals.
X###
X
X@monlen = ( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
X$tot_string = "zzZ_total";
X$revnum = "1.14";
X
X$base       = 3;      # Base is hours
X$range      = 2;      # Range is days
X$rep_range  = 1;      # Report range is months
X$nominal    = 8.0;    # Nominal is 8.0 hours
X$precision  = 2;      # Precision is 2 decimal places
X
X# The timecard database is either in ~/.timecard, or $TIMECARD.
X
Xif (! $timecard) {
X    $timecard = $ENV{'TIMECARD'} ? $ENV{'TIMECARD'} : "$ENV{'HOME'}/.timecard";
X}
X$lockfile = "$timecard.lock";
X
X# 'base' uses the divisor to calculate amount, while 'range' uses the
X# format to display the report.
X
X%code_to_index = ( "Y", 0, "M", 1, "D", 2, "h", 3, "m", 4, "s", 5 );
X
X@report_format = (
X#   WIDTH   DIVISOR          FORMAT
X    '0    ;  0             ;  $tY',
X    '0    ;  0             ;  $tM/$tY',
X    '4    ;  60 * 60 * 24  ;  $tM/$tD/$tY',
X    '5    ;  60 * 60       ;  $tM/$tD/$tY $th',
X    '8    ;  60            ;  $tM/$tD/$tY $th:$tm',
X    '10   ;  1             ;  $tM/$tD/$tY $th:$tm:$ts',
X);
X
X# Processing of command options is table driven to make things simple
X# and robust.
X
X@opt_switches = (
X# OPT : LONG : ARGS : IN-FILE : ACTION : VALIDATION : VALERRMSG : DOC : HELP
X
X'@@ : usage~ hours [options] [category] [notes]',
X
X'f  : file        : 1 : 0
X    : $timecard = $opt_arg, $lockfile = "$timecard.lock"
X    : -r $timecard
X    : Cannot read file \"$timecard\"
X    : read timecard info from \"file\" :
X',
X
X'b  : base        : 1 : 1
X    : $base = $code_to_index{$opt_arg}
X    : ($base > 1) && ($base <= @report_format)
X    : Base is invalid.  Choose from~ D h m s
X    : set report base :
X',
X
X'g  : range       : 1 : 1
X    : $range = $code_to_index{$opt_arg}, 1
X    : ($range >= 0)
X    : Range is invalid.  Choose from~ Y M D h m s
X    : set range :
X',
X
X'G  : rep_range   : 1 : 1
X    : $rep_range = $code_to_index{$opt_arg}, 1
X    : ($rep_range >= 0)
X    : Report range is invalid.  Choose from~ Y M D h m s
X    : set report range :
X',
X
X'n  : nominal     : 1 : 1 :
X    : $nominal > 0 : Nominal must be greater than 0
X    : set nominal time (default is $nominal) :
X',
X
X'R  : rounding    : 1 : 1 :
X    : $rounding > 0 : Rounding must be greater than 0
X    : set rounding :
X',
X
X'P  : precision   : 1 : 1 :
X    : $precision > 0 : Precision must be greater than 0
X    : set precision :
X',
X
X'l  : time_left   : 0 : 1
X    : $time_left = $just_this = 1
X    : 1 :
X    : print time left till nominal :
X',
X
X'v  : version     : 0 : 1
X    : &report_version()
X    : 1 :
X    : show version number :
X',
X
X'a  : show_all    : 0 : 1 : : 1 : : print summary for entire timecard      : ',
X't  : just_this   : 0 : 1 : : 1 : : print hours worked within this range   : ',
X'r  : relative    : 0 : 1 : : 1 : : print report relative to nominal time  : ',
X'p  : percentages : 0 : 1 : : 1 : : report in percentages                  : ',
X'd  : just_now    : 0 : 1 : : 1 : : display time since last check-in       : ',
X
X'ci : check_in    : 0 : 1
X    : &process_command("i")
X    : 1 :
X    : check in :
X',
X
X'co : check_out   : 0 : 1
X    : &process_command("o")
X    : 1 :
X    : check out (category/notes optional) :
X',
X
X'ca : annotate    : 0 : 1
X    : &process_command("a")
X    : 1 :
X    : annotate last checkout :
X',
X
X'cm : mark        : 0 : 1
X    : &process_command("m")
X    : 1 :
X    : insert a mark at the current time :
X',
X
X'cx : extra       : 0 : 1
X    : &process_command("x")
X    : 1 :
X    : log "extra time" (similar to a checkout) :
X',
X
X'@@ : No arg prints the current range.  A "category" will restrict the report.'
X);
X
X###
X### Process command line options.
X###
X
X# opt_get uses the following globals:
X#    opt_switches    - to process switches in the array
X#    opt_width       - width of "-OPT,--LONG" column
X#    opt_extract     - to read option settings from a file
X
X$opt_extract = '($opt, $opt_arg) =~ /^\.(\w+)\s*(.*)/';
X$opt_width = 20;
X&opt_get(@ARGV);
Xsplice(@ARGV, 0, $opt_ind);
X
X# At this point, it's time for a report.  Define the necessary globals.
X
X@m_context = localtime();
X$now = &mktime(@m_context);
Xsplice(@m_context, 6), $m_context[4]++;
X@m_context = reverse @m_context;
X
X$di = "([0-9]{2})";
X($width, $divisor) = split(/\s*;\s*/, $report_format[$base]);
X($j, $j, $r_format) = split(/\s*;\s*/, $report_format[$range]);
X$width += $precision;
Xeval "\$divisor = $divisor";
X
X###
X### Process the timecard.
X###
X
Xopen(TIMESHEET, $timecard) || die "Can't open \"$timecard\": $!\n";
X
XREADLOOP:
Xwhile (<TIMESHEET>) {
X
X    # All lines of data are of the format: DATE TIME COMMAND ARGS
X
X    ($mon, $day, $year, $hour, $min, $sec, $action, $j, $category, $desc) =
X        /$di\/$di\/$di $di:$di:$di (.)( ([-A-Za-z_0-9\/]+)? ?(.*))?/o;
X
X    # If the action is "m", flush the current totals and set 'mark_mode'
X
X    if ($action eq "m") {
X        undef(%totals);
X        $mark_mode = 1;
X    }
X    elsif ($action eq "i") {
X	@context = ($year, $mon, $day, $hour, $min, $sec);
X	if ($just_this || (! $mark_mode && ! $show_all)) {
X	    for ($i = ($just_this ? $range : $rep_range); $i >= 0; $i--) {
X		next READLOOP if ($context[$i] != $m_context[$i]);
X	    }
X	}
X	$in_time = &mktime($sec, $min, $hour, $day, $mon - 1, $year);
X	$moment_index = &t_index($in_time, $range);
X	@m_context = @context;
X	$in_state = 1;
X    }
X    elsif ($action =~ /^[ox]$/) {
X	next if (! $in_state);
X	if (@ARGV && $category && ! &has_category($ARGV[0], $category)) {
X	    $in_state = 0;
X	    next READLOOP;
X	}
X        $category = $tot_string if (! $category);
X        &bump(&mktime($sec, $min, $hour, $day, $mon - 1, $year));
X	$in_state = 0;
X    }
X    elsif ($action eq "a") {
X	next if (! $moment_index);
X        $totals{$moment_index . ":a"} = "$category $desc";
X    }
X    else {
X        die "line:\n${_}not understood\n";
X    }
X}
Xclose(TIMESHEET);
X
X# If asked for a time report during a check-in, we need to act as
X# though we'd just checked out.
X
X$curr = &bump($now) if ($in_state && ! @ARGV);
X
Xif ($just_now) {
X    if ($in_state) {
X	printf(" %${width}.${precision}f  (%s)\n", &t_base($curr),
X	       &timedstr($curr));
X	exit(0);
X    }
X    else {
X	die "You are not currently checked in.\n";
X    }
X}
X
X###
X### Display the report.
X###
X
Xforeach $project (sort keys %totals) {
X    next if ($just_this && ($project !~ /^\./));
X    next if ($project =~ /:[xa]$/ || ! $project);
X    next if (@ARGV && ($project !~ /^\./) && ($project ne $tot_string) &&
X	     (! &has_category($ARGV[0], $project)));
X
X    $tot = &t_base($totals{$project});
X    $percdisp = 0;
X    $project_name = $project;
X
X    # Projects that begin with a '.' are time ranges.  Otherwise, it's
X    # either a project name, or the $tot_string category.
X
X    if ($project =~ /^\./) {
X        $project_name = &t_format($project);
X        $tot -= ($relative ? $nominal : 0);
X    }
X    elsif ($project ne $tot_string) {
X        $nominal = &t_base($totals{$tot_string});
X    }
X
X    # Percentages report as a percentile of the nominal boundary
X
X    if ($percentages && ($project ne $tot_string)) {
X        $tot /= $nominal;
X        $tot *= 100.0;
X        $percdisp = 1;
X    }
X
X    # Now print the total.  The output is different if we're just
X    # printing one day.
X
X    if (! $just_this) {
X	if ($project eq $tot_string) {
X	    printf("%s  ---------\n", "-" x ($width + 1));
X	}
X        $line = sprintf("%s%${width}.${precision}f  %s", $percdisp ? "%" : " ",
X                        $tot, ($project_name eq $tot_string) ? "total" :
X                        $project_name);
X
X	# Check for extra time in this range, and annotations
X
X	if ($totals{"$project:x"}) {
X	    $line .= sprintf("  + %.${precision}f",
X			     &t_base($totals{"$project:x"}));
X	}
X	if ($totals{"$project:a"}) {
X	    $line .= "  " . $totals{"$project:a"};
X	}
X    }
X    else {
X	$line = (! $time_left ? sprintf("%s%${width}.${precision}f",
X					$percdisp ? "%" : " ", $tot) :
X                 &timedstr(($nominal * $divisor) - $totals{$project}));
X    }
X    print $line, "\n";
X}
Xexit(0);
X
X###
X### Define all subroutines.
X###
X
X# Increment the global tables with today's differences, in the proper
X# category spots.  Argument 1 is the "out time".
X
Xsub bump
X{
X    $diff = $_[0] - $in_time;
X
X    # Extract the category information.  Multiple categories are
X    # separated by slashes (/).
X
X    if ($category ne $tot_string) {
X        $totals{$tot_string} += $diff;
X    }
X    foreach $cat (($category =~ /\//) ? split('/', $category) : ($category)) {
X        $totals{$cat} += $diff;
X    }
X    $moment_index .= ":x" if ($action eq "x");
X    $totals{$moment_index} += $diff;
X    return $diff;
X}
X
X# All of this nonsense is necessary because Larry didn't implement a
X# mktime() function.  Why???
X
Xsub mktime
X{
X    ($tyear, $tdays, $mon) = ($_[5] - 70, 0, $_[4]);
X    for ($i = 0; $i < $mon; $i++) {
X	$tdays += $monlen[$i];
X    }
X    $tdays++ if (($_[5] % 4 == 0) && (($_[5] % 100 != 0) ||
X				      ($_[5] % 400 == 0)));
X    $tdays += ($_[3] + int($tyear / 4) + ($tyear * 365) - 1);
X    return ($_[0] + ($_[1] * 60) + ($_[2] * 60 * 60) +
X	    ($tdays * 24 * 60 * 60));
X}
X
X# Given a second count, turn it into a delta time string
X
Xsub timedstr
X{
X    local($neg) = 0;
X    if ($_[0] < 0) {
X	$neg = 1;
X	$_[0] *= -1;
X    }
X    $D = ($h = ($m = ($s = $_[0]) / 60) / 60) / 24;
X    return sprintf("%s%s%02d:%02d:%02d", $neg ? "- " : "", (int($D) > 0) ?
X                   sprintf("%d days ", $D) : "", $h % 24, $m % 60, $s % 60);
X}
X
X# Return a number relevant to the current base
X
Xsub t_base
X{
X    $result = $_[0] / $divisor;
X    if ($rounding) {
X	$result = &round($result, $rounding);
X    }
X    return $result;
X}
X
X# Given a time moment in seconds past the epoch, return a time index
X# of the form YEAR.MONTH.DAY.HOUR.MINUTE.SECOND, including only those
X# fields which should appear in the report.  This is done to keep the
X# 'totals' array in sorted order.
X
Xsub t_index
X{
X    (@e) = gmtime($_[0]), $e[4]++;  # global for speed
X    splice(@e, 6);
X    @e = reverse @e;
X    for ($r = $range + 1; $r < @report_format; $r++) {
X	$e[$r] = 0;
X    }
X    return sprintf(".%02d.%02d.%02d.%02d.%02d.%02d", @e);
X}
X
X# Given a time index and a range, return the formatted time string.
X
Xsub t_format
X{
X    ($leader, $tY, $tM, $tD, $th, $tm, $ts) = split('\.', $_[0]);
X    return eval "\"$r_format\"";
X}
X
X# Round a floating point number to the nearest given increment.
X
Xsub round
X{
X    $off = $_[0] / $_[1];
X    return (int($off) + ((($off - int($off)) >= 0.50) ? 1 : 0)) * $_[1];
X}
X
X# See if a category name is in the given string
X
Xsub has_category
X{
X    ($catname, $string) = @_;
X    foreach $cat (split('/', $string)) {
X	foreach $cn (split('/', $catname)) {
X	    return 1 if ($cat eq $cn);
X	}
X    }
X    return 0;
X}
X
X# Print out the version number
X
Xsub report_version
X{
X    printf("Hours %s\n", $revnum);
X    exit 0;
X}
X
X# Process an action request
X
Xsub process_command
X{
X    $" = ' ';
X    splice(@ARGV, 0, $opt_ind + 1);
X    if ($_[0] =~ /[ox]/) {
X        die "You're already checked out!\n" if (! -f $lockfile);
X	if (! @ARGV) {
X	    open(LOCKFILE, $lockfile);
X	    chop($ARGV[0] = <LOCKFILE>);
X	    close(LOCKFILE);
X	}
X        unlink($lockfile);
X    }
X    if ($_[0] eq "i") {
X        die "You're already working!\n" if (-f $lockfile);
X	open(LOCKFILE, "> $lockfile");
X	print LOCKFILE "@ARGV\n", undef(@ARGV) if (@ARGV);
X	close(LOCKFILE);
X    }
X    open(TIMESHEET, ">> $timecard") || die "Cannot open $timecard: $!\n";
X    ($sec, $min, $hour, $mday, $mon, $year) = localtime();
X    printf TIMESHEET "%02d/%02d/%02d %02d:%02d:%02d %s @ARGV\n",
X        $mon + 1, $mday, $year, $hour, $min, $sec, $_[0];
X    close(TIMESHEET);
X    exit 0;
X}
X
X# Process an array of options
X
Xsub opt_get
X{
X    local($long_opt, $sing_opt, $wh_opt, $i, $valid);
X    for ($opt_ind = 0, $opt_argi = 1; $opt_ind < @_;
X	 $opt_ind += $opt_argi, $opt_argi = 1) {
X	if ($_[$opt_ind] =~ /^-(-?)((.).*)/) {
X	    ($long_opt, $wh_opt, $sing_opt) = ($1, $2, $3);
X	    do {
X		if (($sing_opt =~ /[h?]/) || ($long_opt eq "help")) {
X		    &show_usage();
X		    exit(0);
X		}
X		$valid = 0;
X		undef($opt_arg);
X		undef(@opt_arg);
X		foreach $line (@opt_switches) {
X		    ($short, $long, $args, $infile, $action, $validate,
X		     $valerr, $docstring, $help) = split(/\s*:\s*/, $line);
X		    $docstring =~ s/\~/:/g;
X		    $help =~ s/\~/:/g;
X		    $valerr =~ s/\~/:/g;
X		    $long =~ s/_/-/g;
X		    if (($long_opt && ($long eq $wh_opt)) ||
X			(! $long_opt &&
X			 (($short eq $sing_opt) ||
X			  ($short eq substr($wh_opt, 0, length($short)))))) {
X			$valid = length($short);
X			if ($args == 1) {
X			    $opt_arg = $_[$opt_ind + $opt_argi++];
X			}
X			elsif ($args > 1) {
X			    undef @opt_arg;
X			    for ($i = 0; $i < $args; $i++) {
X				push(@opt_arg, $_[$opt_ind + $opt_argi + $i]);
X			    }
X			    $opt_argi += $i;
X			}
X			if ($action) {
X			    die "$@" if (! eval $action);
X			}
X			else {
X			    $long =~ s/-/_/g;
X			    if ($args == 1) {
X				eval "\$$long = \$opt_arg";
X			    }
X			    elsif ($args > 1) {
X				eval "\@$long = \@opt_arg";
X			    }
X			    else {
X				eval "\$$long = 1";
X			    }
X			}
X			if (! eval $validate) {
X			    eval "\$valerr = \"$valerr\"";
X			    die "$valerr\n";
X			}
X		    }
X		}
X		if (! $valid) {
X		    die "Unknown option: $_[$opt_ind]\n";
X		}
X		$wh_opt = substr($wh_opt, $valid);
X		$sing_opt = substr($wh_opt, 0, 1);
X
X		# After the first loop through, there can't be any
X		# long options in the current word.
X
X		$long_opt = 0;
X
X	    } until (! length($wh_opt));
X	}
X	else {
X	    last;
X	}
X    }
X}
X
X# Given an array of options, generate a "usage" screen
X
Xsub show_usage
X{
X    foreach $line (@opt_switches) {
X	($short, $long, $args, $infile, $action, $validate,
X	 $valerr, $docstring, $help) = split(/\s*:\s*/, $line);
X	$docstring =~ s/\~/:/g;
X	eval "\$docstring = \"$docstring\"";
X	if ($short eq "@@") {
X	    $long =~ s/\~/:/g;
X	    eval "\$long = $long";
X	    print $long, "\n";
X	}
X	else {
X	    $long =~ s/_/-/g;
X	    printf("    %-${opt_width}s %s\n",
X		   sprintf("-%s,--%s", $short, $long), $docstring);
X	}
X    }
X}
END_OF_FILE
  if test 14035 -ne `wc -c <'hours'`; then
    echo shar: \"'hours'\" unpacked with wrong size!
  fi
  chmod +x 'hours'
  # end of 'hours'
fi
if test -f 'hours.1' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'hours.1'\"
else
  echo shar: Extracting \"'hours.1'\" \(10138 characters\)
  sed "s/^X//" >'hours.1' <<'END_OF_FILE'
X.rn '' }`
X''' $RCSfile: hours.1,v $$Revision: 1.4 $$Date: 1995/02/11 07:01:57 $
X''' 
X.de Sh
X.br
X.if t .Sp
X.ne 5
X.PP
X\fB\\$1\fR
X.PP
X..
X.de Sp
X.if t .sp .5v
X.if n .sp
X..
X.de Ip
X.br
X.ie \\n(.$>=3 .ne \\$3
X.el .ne 3
X.IP "\\$1" \\$2
X..
X.de Vb
X.ft CW
X.nf
X.ne \\$1
X..
X.de Ve
X.ft R
X
X.fi
X..
X'''
X'''
X'''     Set up \*(-- to give an unbreakable dash;
X'''     string Tr holds user defined translation string.
X'''     Bell System Logo is used as a dummy character.
X'''
X.tr \(*W-|\(bv\*(Tr
X.ie n \{\
X.ds -- \(*W-
X.if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
X.if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch
X.ds L" ""
X.ds R" ""
X.ds L' '
X.ds R' '
X'br\}
X.el\{\
X.ds -- \(em\|
X.tr \*(Tr
X.ds L" ``
X.ds R" ''
X.ds L' `
X.ds R' '
X.if t .ds PI \(*p
X.if n .ds PI PI
X'br\}
X.TH HOURS 1 "\*(RP"
X.UC
X.if n .hy 0 
X.if n .na
X.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
X.de CQ          \" put $1 in typewriter font
X.ft CW
X'if n "\c
X'if t \\&\\$1\c
X'if n \\&\\$1\c
X'if n \&"
X\\&\\$2 \\$3 \\$4 \\$5 \\$6 \\$7
X'.ft R
X..
X.\" @(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2
X.	\" AM - accent mark definitions
X.bd S B 3
X.	\" fudge factors for nroff and troff
X.if n \{\
X.	ds #H 0
X.	ds #V .8m
X.	ds #F .3m
X.	ds #[ \f1
X.	ds #] \fP
X.\}
X.if t \{\
X.	ds #H ((1u-(\\\\n(.fu%2u))*.13m)
X.	ds #V .6m
X.	ds #F 0
X.	ds #[ \&
X.	ds #] \&
X.\}
X.	\" simple accents for nroff and troff
X.if n \{\
X.	ds ' \&
X.	ds ` \&
X.	ds ^ \&
X.	ds , \&
X.	ds ~ ~
X.	ds ? ?
X.	ds ! !
X.	ds / 
X.	ds q 
X.\}
X.if t \{\
X.	ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u"
X.	ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u'
X.	ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u'
X.	ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u'
X.	ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u'
X.	ds ? \s-2c\h'-\w'c'u*7/10'\u\h'\*(#H'\zi\d\s+2\h'\w'c'u*8/10'
X.	ds ! \s-2\(or\s+2\h'-\w'\(or'u'\v'-.8m'.\v'.8m'
X.	ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u'
X.	ds q o\h'-\w'o'u*8/10'\s-4\v'.4m'\z\(*i\v'-.4m'\s+4\h'\w'o'u*8/10'
X.\}
X.	\" troff and (daisy-wheel) nroff accents
X.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V'
X.ds 8 \h'\*(#H'\(*b\h'-\*(#H'
X.ds v \\k:\h'-(\\n(.wu*9/10-\*(#H)'\v'-\*(#V'\*(#[\s-4v\s0\v'\*(#V'\h'|\\n:u'\*(#]
X.ds _ \\k:\h'-(\\n(.wu*9/10-\*(#H+(\*(#F*2/3))'\v'-.4m'\z\(hy\v'.4m'\h'|\\n:u'
X.ds . \\k:\h'-(\\n(.wu*8/10)'\v'\*(#V*4/10'\z.\v'-\*(#V*4/10'\h'|\\n:u'
X.ds 3 \*(#[\v'.2m'\s-2\&3\s0\v'-.2m'\*(#]
X.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#]
X.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H'
X.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u'
X.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#]
X.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#]
X.ds ae a\h'-(\w'a'u*4/10)'e
X.ds Ae A\h'-(\w'A'u*4/10)'E
X.ds oe o\h'-(\w'o'u*4/10)'e
X.ds Oe O\h'-(\w'O'u*4/10)'E
X.	\" corrections for vroff
X.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u'
X.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u'
X.	\" for low resolution devices (crt and lpr)
X.if \n(.H>23 .if \n(.V>19 \
X\{\
X.	ds : e
X.	ds 8 ss
X.	ds v \h'-1'\o'\(aa\(ga'
X.	ds _ \h'-1'^
X.	ds . \h'-1'.
X.	ds 3 3
X.	ds o a
X.	ds d- d\h'-1'\(ga
X.	ds D- D\h'-1'\(hy
X.	ds th \o'bp'
X.	ds Th \o'LP'
X.	ds ae ae
X.	ds Ae AE
X.	ds oe oe
X.	ds Oe OE
X.\}
X.rm #[ #] #H #V #F C
X.SH "NAME"
X.B hours
X\- time interval tracking program
X.SH "SYNOPSIS"
Xhours [options] [category] [notes]
X.Vb 7
X\&    -f,--file            read timecard info from "file"
X\&    -b,--base            set report base
X\&    -g,--range           set range
X\&    -G,--rep-range       set report range
X\&    -n,--nominal         set nominal time (default is 8)
X\&    -R,--rounding        set rounding
X\&    -P,--precision       set precision
X\&    -l,--time-left       print time left till nominal
X\&    -v,--version         show version number
X\&    -a,--show-all        print summary for entire timecard
X\&    -t,--just-this       print hours worked within this range
X\&    -r,--relative        print report relative to nominal time
X\&    -p,--percentages     report in percentages
X\&    -d,--just-now        display time since last check-in
X\&    -ci,--check-in       check in
X\&    -co,--check-out      check out (category/notes optional)
X\&    -ca,--annotate       annotate last checkout
X\&    -cm,--mark           insert a mark at the current time
X\&    -cx,--extra          log "extra time" (similar to a checkout)
X.Ve
XInvoking the program with no arguments will print the totals for the
Xcurrent report range.  Specifying a category will restrict the report to
Xthat category only.  \-a can be used to scan the entire timecard file.
X.PP
X.SH "DESCRIPTION"
X"hours" is a time\-interval tracker.  Use it whenever you want to keep
Xtrack of time accumulated between past events.  The most common type
Xof time\-interval data people come in contact with is their timecard.
XOther types include:
X.PP
X.Vb 5
X\&    Internet "on" time
X\&    Operating system "up" time
X\&    Long-distance connect time
X\&    Machine time spent during compilations
X.Ve
XSince timecard tracking is the most common use of "hours", let's look
Xat the options in that light.
X.PP
X.SH "OPTIONS"
X.TP
X.B \-f
XCauses "hours" to read timecard data from the specified file.
X.TP
X.B \-b
XSets the reporting base.  This format is used when displaying how much
Xtime you've worked.  The default base is hours.  Acceptable bases are:
Xseconds, minutes, hours, days.  You can select a base using the one of
Xthe following letters: "s", "m", "h", "D".  These stand for "seconds",
X"minutes", "hours" and "days", respectively.
X.TP
X.B \-g
XSets the reporting range.  When printing a full report (i.e., hours
Xwith no arguments), time is printed in base units per range per report
Xrange.
X
XFor example, let's say you chose HOURS for the base, DAYS for the
Xrange, and MONTHS for the report range (see next item).  This is the
Xdefault.  The report will show how many hours you've worked per day in
Xthe current month.
X
XBy changing to MINUTES, HOURS and YEARS for the base, range and report
Xrange respectively, you'll get a report in terms of minutes per hour
Xfor the current year.  Remember that a report shows time since the
Xlast checkin, so you may see more than 60 minutes in an hour.  If this
Xall seems a bit complex, give the program a run with several different
Xcombinations of bases and ranges.  After a moment you'll see what's
Xgoing on.
X
XOne more item: you can set your range and report range smaller than
Xthe base.  To show how this would be useful, consider this: you eat
Xlunch around 12 each day, and check-in around 8.  But you want to know
Xif you spend exactly 4 hours before eating lunch.  How do you find
Xthis out?  By selecting HOURS for the base, MINUTES for the range, and
XMONTHS for the report range.  This report will show you how many hours
Xyou work after each check-in, with the range displayed down to the
Xminute.
X
XThis is all really hard to explain in a man page; give it try
Xyourself, you'll see what I mean.
X.TP
X.B \-G
XSets the report range.  See above.
X.TP
X.B \-n
XSets the notion of what a "nominal" period is.  At the moment, this
Xrepresents base units per range.  The default is 8 hours per day.
XThere is no notion of "8 hours per working day, 0 on the weekends,"
Xhowever.  That feature is in the works.
X.TP
X.B \-R
XSets the rounding factor.  All base values will be rounded to the
Xincrement given.
X.TP
X.B \-P
XSets the base precision.  This can be valuable if your rounding is set
Xto the nearest 0.5, and you don't care to see the second decimal.
XIt's also nice with higher interval bases (e.g., days per month in the
Xcurrent year).
X.TP
X.B \-l
XShows how many hours/minutes/seconds you have left until you've
Xcompleted your nominal time for the current range.  This usually means
X"when you can go home."  Not all bosses support this definition,
Xhowever.
X.TP
X.B \-v
XDisplays the version of the "hours" program.
X.TP
X.B \-a
XReport on all data in the timecard.  This report is usually
Xmeaningless in and of itself, but it becomes valuable when used in
Xconjunction with a category constraint.  For example, if you've been
Xworking on the "ada" project for three years, and want to know how
Xmany hours you've actually spent on it, you can say "hours \-a ada".
X.TP
X.B \-t
XShows the amount of time worked in the current range.  Usually means
Xthe number of hours worked today.
X.TP
X.B \-r
XDisplays relative times in the report instead of absolute times.  The
Xunits in the report will be relative to the nominal for each "range".
XIn the default case, it will show you how many hours worked over or
Xunder 8 hours each day.  Useful for viewing overtime at-a-glance.
X.TP
X.B \-p
XInstead of showing hours or relative hours in the report, show
Xpercentages of the nominal.  Sub-totals will become percentages of the
Xgrand total.
X.TP
X.B \-d
XShows the amount of time worked since the last check-in.  Compare with
Xthe
X.B \-t
XOption which reports on the current range (maybe MONTHS!).
X.TP
X.B \-ci
XChecks in.
X.TP
X.B \-co
XChecks out.
X.TP
X.B \-ca
XAnnotates the last check-out.  If, for some reason, your time value is
Xreally off for a certain range, you can annotate it to give an
Xexplanation.  Only one annotation per range is supported!
X.TP
X.B \-cm
XSets a mark in the timecard at the current moment.  This overrides the
Xmeaning of "report range" to mean "back in time until the last mark."
XVery useful if your company collects timecards at odd intervals (15
Xdays, 2 weeks, etc.).
X.TP
X.B \-cx
XLogs "extra" time.  Use this instead of a checkout when you don't want
Xthe time logged to count towards your totals.  I find this useful for
Xprojects at home that aren't counted toward my paycheck, but whose
Xtime should be logged anyway (for tracking purposes).
X.PP
X.SH "NOTES"
XThat's about all there is to it.  If you have suggestions or feature
Xrequests, please send me e-mail!
X.PP
X.SH "BUGS"
XThe notion of "nominal" and "relative" are really weak.
X.PP
XIt's hard to describe the difference between "base", "range" and
X"report range".  Trial by example seems to be the best way of
Xexplaining them.
X.PP
X.SH "Author"
X.Vb 10
X\&John Wiegley <johnw@peaceful.org>
X.Ve
X.PP
X02/10/95
X
X.rn }` ''
END_OF_FILE
  if test 10138 -ne `wc -c <'hours.1'`; then
    echo shar: \"'hours.1'\" unpacked with wrong size!
  fi
  # end of 'hours.1'
fi
if test -f 'left' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'left'\"
else
  echo shar: Extracting \"'left'\" \(27 characters\)
  sed "s/^X//" >'left' <<'END_OF_FILE'
X#!/bin/sh
Xexec hours -l $@
END_OF_FILE
  if test 27 -ne `wc -c <'left'`; then
    echo shar: \"'left'\" unpacked with wrong size!
  fi
  chmod +x 'left'
  # end of 'left'
fi
if test -f 'tann' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'tann'\"
else
  echo shar: Extracting \"'tann'\" \(28 characters\)
  sed "s/^X//" >'tann' <<'END_OF_FILE'
X#!/bin/sh
Xexec hours -ca $@
END_OF_FILE
  if test 28 -ne `wc -c <'tann'`; then
    echo shar: \"'tann'\" unpacked with wrong size!
  fi
  chmod +x 'tann'
  # end of 'tann'
fi
if test -f 'ti' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'ti'\"
else
  echo shar: Extracting \"'ti'\" \(28 characters\)
  sed "s/^X//" >'ti' <<'END_OF_FILE'
X#!/bin/sh
Xexec hours -ci $@
END_OF_FILE
  if test 28 -ne `wc -c <'ti'`; then
    echo shar: \"'ti'\" unpacked with wrong size!
  fi
  chmod +x 'ti'
  # end of 'ti'
fi
if test -f 'tmark' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'tmark'\"
else
  echo shar: Extracting \"'tmark'\" \(28 characters\)
  sed "s/^X//" >'tmark' <<'END_OF_FILE'
X#!/bin/sh
Xexec hours -cm $@
END_OF_FILE
  if test 28 -ne `wc -c <'tmark'`; then
    echo shar: \"'tmark'\" unpacked with wrong size!
  fi
  chmod +x 'tmark'
  # end of 'tmark'
fi
if test -f 'to' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'to'\"
else
  echo shar: Extracting \"'to'\" \(28 characters\)
  sed "s/^X//" >'to' <<'END_OF_FILE'
X#!/bin/sh
Xexec hours -co $@
END_OF_FILE
  if test 28 -ne `wc -c <'to'`; then
    echo shar: \"'to'\" unpacked with wrong size!
  fi
  chmod +x 'to'
  # end of 'to'
fi
if test -f 'ts' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'ts'\"
else
  echo shar: Extracting \"'ts'\" \(234 characters\)
  sed "s/^X//" >'ts' <<'END_OF_FILE'
X#!/bin/sh
X#
X# NOTE: This script only works correctly if the '-f' flag is not
X#       present.
X
Xif test -s $TIMECARD.lock -o -s $HOME/.timecard.lock; then
X    hours -co
X    exec hours -ci $@
Xelse
X    hours -co $@
X    exec hours -ci
Xfi
END_OF_FILE
  if test 234 -ne `wc -c <'ts'`; then
    echo shar: \"'ts'\" unpacked with wrong size!
  fi
  chmod +x 'ts'
  # end of 'ts'
fi
if test -f 'txtra' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'txtra'\"
else
  echo shar: Extracting \"'txtra'\" \(28 characters\)
  sed "s/^X//" >'txtra' <<'END_OF_FILE'
X#!/bin/sh
Xexec hours -cx $@
END_OF_FILE
  if test 28 -ne `wc -c <'txtra'`; then
    echo shar: \"'txtra'\" unpacked with wrong size!
  fi
  chmod +x 'txtra'
  # end of 'txtra'
fi
echo shar: End of archive 1 \(of 1\).
cp /dev/null ark1isdone
MISSING=""
for I in 1 ; do
    if test ! -f ark${I}isdone ; then
	MISSING="${MISSING} ${I}"
    fi
done
if test "${MISSING}" = "" ; then
    echo You have the archive.
    rm -f ark[1-9]isdone
else
    echo You still must unpack the following archives:
    echo "        " ${MISSING}
fi
exit 0
exit 0 # Just in case...

