Skip to content

Instantly share code, notes, and snippets.

@amotl
Last active October 1, 2015 11:07
Show Gist options
  • Save amotl/1980801 to your computer and use it in GitHub Desktop.
Save amotl/1980801 to your computer and use it in GitHub Desktop.
hours.pl - calculates hour summary from textfile with certain formatting
#!/usr/bin/env perl
# -*- coding: utf-8 -*-
# hours.pl
# calculates hour summary from textfile with certain formatting
#
# (c) 2007,2008,2010 Andreas Motl <andreas.motl@ilo.de>
=pod
Possible "hourlines" are e.g.:
Mo, 11.01.2010 18:15-19:30
[amo] Mo, 22.03.2010 14:15-18:15
[amo] Fr, 12.03.2010 xx:xx-xx:xx: 2h
[amo] Fr, 12.03.2010: 2h
Features:
- does multi-person: see hourline-formatting above ([amo])
- does multi-project: please designate in "Tasks.txt" using line like
@project: MyProjectNameOrIdentifier
- stores hourly rates in ~/.hours.conf
- remembers project identifiers (path to Tasks.txt) in ~/.hours.conf
- TODO: --report-total option scans all projects listed in ~/.hours.conf
- TODO: invoicing feature - somehow mark as billed:
- mark individual hourlines
- mark complete file
- rename file to "Tasks_yyyy-mm-dd.txt"
=cut
use strict;
use warnings;
use Data::Dumper;
use Config::IniHash;
use File::Spec;
use Config;
my $home_env_var = 'HOME';
$home_env_var = 'USERPROFILE' if $Config{'osname'} =~ m/mswin/i;
# ------------------------------------------
# configuration settings
# ------------------------------------------
my $CONFIG_FILE = "$ENV{$home_env_var}/.hours.conf";
my $HOURLINE_PREFIX = ' ';
# ------------------------------------------
sub time_to_hours {
my $time = shift;
(my $hours, my $minutes) = split(':', $time);
$minutes /= 60;
return $hours + $minutes;
}
sub hours_diff {
my $begin = shift;
my $end = shift;
my $diff = $end - $begin;
$diff += 24 if $begin > $end;
return $diff;
}
sub parse_hoursfile {
my $hoursfile = shift;
open(FH, '<' . $hoursfile);
my $project = 'default';
my $hours = {};
my $project_pattern = '@project: (?<project>.+)$';
my $pattern_prefix = $HOURLINE_PREFIX . '(?:\[(?<person>.+)\] )?\D\D, \d+\.\d+\.\d\d\d\d ';
while (<FH>) {
chomp;
#print $_, "\n";
if (m/$project_pattern/) {
$project = $+{'project'} if ($+{'project'});
#exit;
}
my $person = 'default';
# parse hours directly from line
if (m/^$pattern_prefix.+\s(?<hours>[\.0-9]+?)h$/) {
$person = $+{'person'} if ($+{'person'});
$hours->{$person} += $+{'hours'};
# compute hours
} elsif (m/^$pattern_prefix(?<begin>\d\d:\d\d)-(?<end>\d\d:\d\d)/) {
$person = $+{'person'} if ($+{'person'});
my $begin_hours = time_to_hours($+{'begin'});
my $end_hours = time_to_hours($+{'end'});
my $hours_diff = hours_diff($begin_hours, $end_hours);
#print "hours_line: $begin-$end: $begin_hours-$end_hours => ${hours_diff}h", "\n";
$hours->{$person} += $hours_diff;
}
#print "Hours by now: ", $hours, "\n";
}
close(FH);
#if ($project eq 'default') {
# if (not ask("WARNING: Project name via '\@project: {identifier}' not defined in task file, will use 'default'!\nContinue [y|n]?")) {
# exit;
# }
#}
return $project, $hours;
}
sub generate_report {
my $project = shift;
my $hours_by_person = shift;
my $rates_by_person = shift;
print "=" x 42, "\n";
print "Project: $project\n";
print "=" x 42, "\n";
print "\n";
foreach my $person (keys %$hours_by_person) {
my $hours = $hours_by_person->{$person};
my $rate = $rates_by_person->{$person};
print "-" x 42, "\n";
print "Report for: $person", "\n";
print "-" x 42, "\n";
generate_person_summary($person, $hours, $rate);
print "\n";
}
}
sub generate_person_summary {
my $person = shift;
my $hours = shift;
my $rate = shift;
my $hours_per_day = 8;
my $days_per_week = 5;
my $weeks_per_month = 4;
my $days = $hours / $hours_per_day;
my $months = $days / ($weeks_per_month * $days_per_week);
my $amount = $hours * $rate;
print "Hours: ", $hours, "\n";
print "Days: ", sprintf('%.02f', $days), "\t", "($hours_per_day hours per day, rounded)", "\n";
print "Months: ", sprintf('%.02f', $months), "\t", "($days_per_week days per week, rounded)", "\n";
print "Amount: ", sprintf('%.02f', $amount), "\t", "(", sprintf('%.02f', $rate), " per hour)", "\n";
}
sub config_get_hourly_rates {
my $project = shift;
my $persons = shift;
my $rates = {};
foreach my $person (@$persons) {
$rates->{$person} = config_get_hourly_rate($project, $person);
}
return $rates;
}
sub config_get_hourly_rate {
my $project = shift;
my $person = shift;
#print $project, "\n";
my $config = ReadINI($CONFIG_FILE);
my $rate = $config->{$project}->{"rate-$person"};
if (not $rate) {
print "Please enter hourly rate for '$person\@$project': ";
$rate = <STDIN>;
chomp($rate);
#print "rate: '$rate'", "\n";
$config->{$project}->{"rate-$person"} = $rate;
WriteINI($CONFIG_FILE, $config);
}
return $rate;
}
# also remember path to Tasks.txt file in config
sub config_update_taskfile_path {
my $project = shift;
my $taskfile = shift;
my $config = ReadINI($CONFIG_FILE);
my $config_taskfile = $config->{$project}->{'taskfile'};
my $do_update = 0;
if ($config_taskfile and ($taskfile ne $config_taskfile)) {
if (ask("WARNING: Task file stored in .hours.conf ('$config_taskfile') differs from specified task file!\nUpdate [y|n]?")) {
$do_update = 1;
}
}
if (not $config_taskfile or $do_update) {
$config->{$project}->{'taskfile'} = $taskfile;
WriteINI($CONFIG_FILE, $config);
}
}
sub ask {
my $question = shift;
print $question, ' ';
my $answer = <STDIN>;
chomp($answer);
if (lc($answer) eq 'y') {
return 1;
}
}
sub main {
my $inputfile = $ARGV[0];
die "ERROR: Task file not given!" if not $inputfile;
$inputfile = File::Spec->rel2abs($inputfile);
die "ERROR: Task file '$inputfile' does not exist!" if not -e $inputfile;
print "INFO: Using task file '$inputfile'", "\n";
print "INFO: Using hourly rates from '$CONFIG_FILE'", "\n";
print "\n";
# 1. task file parsing
(my $project, my $hours_by_person) = parse_hoursfile($inputfile);
# 2. correlate with config file (hourly rates, etc.)
#config_update_taskfile_path($project, $inputfile);
my @persons = sort keys %$hours_by_person;
my $rates_by_person = config_get_hourly_rates($project, \@persons);
#print Dumper($rates_by_person);
#exit;
# 3. output report summary
generate_report($project, $hours_by_person, $rates_by_person);
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment