Last active
October 1, 2015 11:07
-
-
Save amotl/1980801 to your computer and use it in GitHub Desktop.
hours.pl - calculates hour summary from textfile with certain formatting
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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