timesheet 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +4 -0
 - data/Manifest.txt +24 -0
 - data/README.rdoc +104 -0
 - data/Rakefile +29 -0
 - data/bin/timesheet +7 -0
 - data/lib/timesheet.rb +82 -0
 - data/lib/timesheet/range_extensions.rb +24 -0
 - data/lib/timesheet/time_entry.rb +50 -0
 - data/lib/timesheet/time_log.rb +71 -0
 - data/lib/timesheet/time_report.rb +113 -0
 - data/lib/timesheet/timesheet_parser.rb +242 -0
 - data/lib/timesheet/trollop.rb +761 -0
 - data/script/console +10 -0
 - data/script/destroy +14 -0
 - data/script/generate +14 -0
 - data/spec/range_extensions_spec.rb +17 -0
 - data/spec/spec.opts +1 -0
 - data/spec/spec_helper.rb +17 -0
 - data/spec/time_entry_spec.rb +137 -0
 - data/spec/time_log_spec.rb +164 -0
 - data/spec/time_report_spec.rb +104 -0
 - data/spec/timesheet_parser_spec.rb +235 -0
 - data/spec/timesheet_spec.rb +188 -0
 - data/tasks/rspec.rake +21 -0
 - metadata +274 -0
 
    
        data/History.txt
    ADDED
    
    
    
        data/Manifest.txt
    ADDED
    
    | 
         @@ -0,0 +1,24 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            History.txt
         
     | 
| 
      
 2 
     | 
    
         
            +
            Manifest.txt
         
     | 
| 
      
 3 
     | 
    
         
            +
            README.rdoc
         
     | 
| 
      
 4 
     | 
    
         
            +
            Rakefile
         
     | 
| 
      
 5 
     | 
    
         
            +
            bin/timesheet
         
     | 
| 
      
 6 
     | 
    
         
            +
            lib/timesheet.rb
         
     | 
| 
      
 7 
     | 
    
         
            +
            lib/timesheet/range_extensions.rb
         
     | 
| 
      
 8 
     | 
    
         
            +
            lib/timesheet/time_entry.rb
         
     | 
| 
      
 9 
     | 
    
         
            +
            lib/timesheet/time_log.rb
         
     | 
| 
      
 10 
     | 
    
         
            +
            lib/timesheet/time_report.rb
         
     | 
| 
      
 11 
     | 
    
         
            +
            lib/timesheet/timesheet_parser.rb
         
     | 
| 
      
 12 
     | 
    
         
            +
            lib/timesheet/trollop.rb
         
     | 
| 
      
 13 
     | 
    
         
            +
            script/console
         
     | 
| 
      
 14 
     | 
    
         
            +
            script/destroy
         
     | 
| 
      
 15 
     | 
    
         
            +
            script/generate
         
     | 
| 
      
 16 
     | 
    
         
            +
            spec/range_extensions_spec.rb
         
     | 
| 
      
 17 
     | 
    
         
            +
            spec/spec.opts
         
     | 
| 
      
 18 
     | 
    
         
            +
            spec/spec_helper.rb
         
     | 
| 
      
 19 
     | 
    
         
            +
            spec/time_entry_spec.rb
         
     | 
| 
      
 20 
     | 
    
         
            +
            spec/time_log_spec.rb
         
     | 
| 
      
 21 
     | 
    
         
            +
            spec/time_report_spec.rb
         
     | 
| 
      
 22 
     | 
    
         
            +
            spec/timesheet_parser_spec.rb
         
     | 
| 
      
 23 
     | 
    
         
            +
            spec/timesheet_spec.rb
         
     | 
| 
      
 24 
     | 
    
         
            +
            tasks/rspec.rake
         
     | 
    
        data/README.rdoc
    ADDED
    
    | 
         @@ -0,0 +1,104 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            = timesheet
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            * http://github.com/jschank/timesheet
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            == DESCRIPTION:
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            <i>Timesheet</i> is simple ruby application for tracking time spent on projects.
         
     | 
| 
      
 8 
     | 
    
         
            +
            It is a console application that uses a simple text file storage back-end (YAML::Store)
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            The main idea is to be able to produce reports of hours spent, such that I can use geektool to display the reports.
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            == FEATURES/PROBLEMS:
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
            with <i>timesheet</i> you can:
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
            * <b>add</b> new entries to the database
         
     | 
| 
      
 17 
     | 
    
         
            +
            * <b>edit</b> existing entries
         
     | 
| 
      
 18 
     | 
    
         
            +
            * <b>delete</b> existing entries
         
     | 
| 
      
 19 
     | 
    
         
            +
            * <b>list</b> existing entries, and specify a time range
         
     | 
| 
      
 20 
     | 
    
         
            +
            * <b>produce</b> simple reports
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            == SYNOPSIS:
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            <i>Timesheet</i> is a script for keeping track of time spent on various projects.
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            === Usage:
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
            	timesheet [OPTIONS] COMMAND [ARGS]
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
            COMMAND is any of the following:
         
     | 
| 
      
 31 
     | 
    
         
            +
            	add
         
     | 
| 
      
 32 
     | 
    
         
            +
            	edit
         
     | 
| 
      
 33 
     | 
    
         
            +
            	delete
         
     | 
| 
      
 34 
     | 
    
         
            +
            	list
         
     | 
| 
      
 35 
     | 
    
         
            +
            	report
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
            OPTIONS are:
         
     | 
| 
      
 38 
     | 
    
         
            +
            <tt>--debug, -d</tt>:: Show debugging information while processing
         
     | 
| 
      
 39 
     | 
    
         
            +
            <tt>--version, -v</tt>:: Print version and exit
         
     | 
| 
      
 40 
     | 
    
         
            +
            <tt>--help, -h</tt>:: Show this message
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
            === For help run:
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
             	timesheet COMMAND --help
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
            for more information on a specific command.
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
            === Your Data
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
            your <i>timesheet</i> data will be stored in a hidden directory under your user account. <i>Timesheet</i> figures out
         
     | 
| 
      
 51 
     | 
    
         
            +
            where this is by referencing the "HOME" environment variable. The default location is therefore:
         
     | 
| 
      
 52 
     | 
    
         
            +
            <tt>/Users/someuser/.timesheet/store.yaml</tt>
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
            You may override this location to any specific location you want by setting the "TIMESHEET_DATA_FILE" environment variable.
         
     | 
| 
      
 55 
     | 
    
         
            +
            This should be the full path to where you want the data stored. Including filename and extension.
         
     | 
| 
      
 56 
     | 
    
         
            +
            You only need to set this if you are unsatisfied with the default location.
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
            === the .idea folder
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
            This project was created using RubyMine 2.0, and that IDE stores its state in the .idea folder.
         
     | 
| 
      
 61 
     | 
    
         
            +
            If you aren't using RubyMine, you can delete it.
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
            == REQUIREMENTS:
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
            * FIX (list of requirements)
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
            == INSTALL:
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
              sudo gem install timesheet
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
            == To Do
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
            - Add indicator in reports for start or end times which had to be trimmed
         
     | 
| 
      
 74 
     | 
    
         
            +
            - Add indicator in reports that exclude comments, that comments are available, when they are, i.e. after a summary,
         
     | 
| 
      
 75 
     | 
    
         
            +
            you might want to know that you should do a detail, or byday report
         
     | 
| 
      
 76 
     | 
    
         
            +
            - Add option to produce pdf file, for this to work, I need to ensure that all the reports are using <b>ruport</b>.
         
     | 
| 
      
 77 
     | 
    
         
            +
            - If comments get too long, we may want to exclude them from the detail report
         
     | 
| 
      
 78 
     | 
    
         
            +
            - Add a new command to inspect a single entry. Just dump all the fields for a given record number
         
     | 
| 
      
 79 
     | 
    
         
            +
            - Might want to track comments separately and/or make an entries comments an array rather than a single comment.
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
            == LICENSE:
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
            (The MIT License)
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
            Copyright (c) 2009 John F. Schank III
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
            Permission is hereby granted, free of charge, to any person obtaining
         
     | 
| 
      
 88 
     | 
    
         
            +
            a copy of this software and associated documentation files (the
         
     | 
| 
      
 89 
     | 
    
         
            +
            'Software'), to deal in the Software without restriction, including
         
     | 
| 
      
 90 
     | 
    
         
            +
            without limitation the rights to use, copy, modify, merge, publish,
         
     | 
| 
      
 91 
     | 
    
         
            +
            distribute, sublicense, and/or sell copies of the Software, and to
         
     | 
| 
      
 92 
     | 
    
         
            +
            permit persons to whom the Software is furnished to do so, subject to
         
     | 
| 
      
 93 
     | 
    
         
            +
            the following conditions:
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
            The above copyright notice and this permission notice shall be
         
     | 
| 
      
 96 
     | 
    
         
            +
            included in all copies or substantial portions of the Software.
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
            THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
         
     | 
| 
      
 99 
     | 
    
         
            +
            EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
         
     | 
| 
      
 100 
     | 
    
         
            +
            MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
         
     | 
| 
      
 101 
     | 
    
         
            +
            IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
         
     | 
| 
      
 102 
     | 
    
         
            +
            CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
         
     | 
| 
      
 103 
     | 
    
         
            +
            TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
         
     | 
| 
      
 104 
     | 
    
         
            +
            SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         
     | 
    
        data/Rakefile
    ADDED
    
    | 
         @@ -0,0 +1,29 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'rubygems'
         
     | 
| 
      
 2 
     | 
    
         
            +
            gem 'hoe', '>= 2.1.0'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require 'hoe'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'fileutils'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require './lib/timesheet'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            Hoe::plugin :newgem
         
     | 
| 
      
 8 
     | 
    
         
            +
            Hoe::plugin :gemcutter
         
     | 
| 
      
 9 
     | 
    
         
            +
            # Hoe.plugin :website
         
     | 
| 
      
 10 
     | 
    
         
            +
            # Hoe.plugin :cucumberfeatures
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            # Generate all the Rake tasks
         
     | 
| 
      
 13 
     | 
    
         
            +
            # Run 'rake -T' to see list of generated tasks (from gem root directory)
         
     | 
| 
      
 14 
     | 
    
         
            +
            $hoe = Hoe.spec 'timesheet' do
         
     | 
| 
      
 15 
     | 
    
         
            +
              self.developer 'John F. Schank III', 'jschank@mac.com'
         
     | 
| 
      
 16 
     | 
    
         
            +
              self.rubyforge_name       = self.name # TODO this is default value
         
     | 
| 
      
 17 
     | 
    
         
            +
              self.extra_deps           = [['activesupport', '>= 0'], ['archive-tar-minitar', '>= 0'], ['chronic', '>= 0'], ['color', '>= 0'], ['fastercsv', '>= 0'], ['hoe', '>= 0'], ['json_pure', '>= 0'], ['newgem', '>= 0'], ['pdf-writer', '>= 0'], ['rake', '>= 0'], ['RedCloth', '>= 0'], ['rich_units', '>= 0'], ['rspec', '>= 0'], ['rubigen', '>= 0'], ['rubyforge', '>= 0'], ['ruport', '>= 0'], ['syntax', '>= 0'], ['transaction-simple', '>= 0']]
         
     | 
| 
      
 18 
     | 
    
         
            +
              # self.extra_deps         = [['activesupport','>= 2.0.2']]
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            require 'newgem/tasks'
         
     | 
| 
      
 23 
     | 
    
         
            +
            Dir['tasks/**/*.rake'].each { |t| load t }
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
            # TODO - want other tests/tasks run by default? Add them to the list
         
     | 
| 
      
 26 
     | 
    
         
            +
            # remove_task :default
         
     | 
| 
      
 27 
     | 
    
         
            +
            # task :default => [:spec, :features]
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
    
        data/bin/timesheet
    ADDED
    
    
    
        data/lib/timesheet.rb
    ADDED
    
    | 
         @@ -0,0 +1,82 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            $:.unshift(File.dirname(__FILE__)) unless
         
     | 
| 
      
 2 
     | 
    
         
            +
              $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            require 'rubygems'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'fileutils'
         
     | 
| 
      
 6 
     | 
    
         
            +
            require 'rich_units'
         
     | 
| 
      
 7 
     | 
    
         
            +
            require 'pathname'
         
     | 
| 
      
 8 
     | 
    
         
            +
            require 'yaml'
         
     | 
| 
      
 9 
     | 
    
         
            +
            require 'yaml/store'
         
     | 
| 
      
 10 
     | 
    
         
            +
            require 'timesheet/range_extensions'
         
     | 
| 
      
 11 
     | 
    
         
            +
            require 'timesheet/time_entry'
         
     | 
| 
      
 12 
     | 
    
         
            +
            require 'timesheet/time_log'
         
     | 
| 
      
 13 
     | 
    
         
            +
            require 'timesheet/time_report'
         
     | 
| 
      
 14 
     | 
    
         
            +
            require 'timesheet/trollop'
         
     | 
| 
      
 15 
     | 
    
         
            +
            require 'timesheet/timesheet_parser'
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            class Timesheet
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
              VERSION = '0.2.0'
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              def self.run(params)
         
     | 
| 
      
 22 
     | 
    
         
            +
                command_hash = {}
         
     | 
| 
      
 23 
     | 
    
         
            +
                command_hash = TimesheetParser.parse(params)
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                raise "Cannot determine location for data. Try timesheet --help for details." unless (ENV["TIMESHEET_DATA_FILE"] || ENV["HOME"])
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                data_file ||= ENV["TIMESHEET_DATA_FILE"]
         
     | 
| 
      
 28 
     | 
    
         
            +
                data_file ||= (ENV["HOME"] + "/.timesheet/store.yaml")
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                FileUtils.makedirs(File.dirname(data_file))
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                store = YAML::Store.new(data_file)
         
     | 
| 
      
 33 
     | 
    
         
            +
                tl = TimeLog.new(store)
         
     | 
| 
      
 34 
     | 
    
         
            +
                ts = Timesheet.new(tl)
         
     | 
| 
      
 35 
     | 
    
         
            +
                ts.process(command_hash)
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
              rescue Exception => e
         
     | 
| 
      
 38 
     | 
    
         
            +
                raise if command_hash[:debug]
         
     | 
| 
      
 39 
     | 
    
         
            +
                puts e.message
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
              end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
              def initialize(timelog)
         
     | 
| 
      
 44 
     | 
    
         
            +
                @timelog = timelog
         
     | 
| 
      
 45 
     | 
    
         
            +
              end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
              def process(command_opts)
         
     | 
| 
      
 48 
     | 
    
         
            +
                command = command_opts[:command]
         
     | 
| 
      
 49 
     | 
    
         
            +
                case command
         
     | 
| 
      
 50 
     | 
    
         
            +
                  when :add    : process_add_command(command_opts)
         
     | 
| 
      
 51 
     | 
    
         
            +
                  when :edit   : process_edit_command(command_opts)
         
     | 
| 
      
 52 
     | 
    
         
            +
                  when :delete : process_delete_command(command_opts)
         
     | 
| 
      
 53 
     | 
    
         
            +
                  when :report : process_report_command(command_opts)
         
     | 
| 
      
 54 
     | 
    
         
            +
                  else raise "Unknown command #{command}"
         
     | 
| 
      
 55 
     | 
    
         
            +
                end
         
     | 
| 
      
 56 
     | 
    
         
            +
              end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
              def process_add_command(command_opts)
         
     | 
| 
      
 59 
     | 
    
         
            +
                te = TimeEntry.new(command_opts[:project], command_opts[:start], command_opts[:end], command_opts[:comment])
         
     | 
| 
      
 60 
     | 
    
         
            +
                @timelog.add(te)
         
     | 
| 
      
 61 
     | 
    
         
            +
              end
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
              def process_edit_command(command_opts)
         
     | 
| 
      
 64 
     | 
    
         
            +
                record_number = command_opts.delete(:record_number)
         
     | 
| 
      
 65 
     | 
    
         
            +
                @timelog.update(record_number, command_opts)
         
     | 
| 
      
 66 
     | 
    
         
            +
              end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
              def process_delete_command(command_opts)
         
     | 
| 
      
 69 
     | 
    
         
            +
                record_number = command_opts.delete(:record_number)
         
     | 
| 
      
 70 
     | 
    
         
            +
                @timelog.delete(record_number)
         
     | 
| 
      
 71 
     | 
    
         
            +
              end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
              def process_report_command(command_opts)
         
     | 
| 
      
 74 
     | 
    
         
            +
                start_time = command_opts[:start]
         
     | 
| 
      
 75 
     | 
    
         
            +
                end_time = command_opts[:end]
         
     | 
| 
      
 76 
     | 
    
         
            +
                entries = @timelog.extract_entries(start_time, end_time)
         
     | 
| 
      
 77 
     | 
    
         
            +
                time_report = TimeReport.new(entries)
         
     | 
| 
      
 78 
     | 
    
         
            +
                time_report.report(command_opts)
         
     | 
| 
      
 79 
     | 
    
         
            +
              end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
            end
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
         @@ -0,0 +1,24 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Thanks to: http://opensoul.org/2007/2/13/ranges-include-or-overlap-with-ranges
         
     | 
| 
      
 2 
     | 
    
         
            +
            class Range
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              def overlap?(range)
         
     | 
| 
      
 6 
     | 
    
         
            +
                self.include?(range.first) || range.include?(self.first)
         
     | 
| 
      
 7 
     | 
    
         
            +
              end
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
              alias_method :include_without_range?, :include?
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
              def include_with_range?(value)
         
     | 
| 
      
 12 
     | 
    
         
            +
                if value.is_a?(Range)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  last = value.exclude_end? ? value.last - 1 : value.last
         
     | 
| 
      
 14 
     | 
    
         
            +
                  self.include?(value.first) && self.include?(last)
         
     | 
| 
      
 15 
     | 
    
         
            +
                else
         
     | 
| 
      
 16 
     | 
    
         
            +
                  include_without_range?(value)
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
              end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
              alias_method :include?, :include_with_range?
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            #  alias_method_chain :include?, :range
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,50 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class TimeEntry 
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
              include Comparable
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              def initialize(project, start_time, end_time, comment = nil)
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                raise ArgumentError.new("Start time must come before end time") if start_time > end_time
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                @project = project
         
     | 
| 
      
 10 
     | 
    
         
            +
                @start_time = start_time
         
     | 
| 
      
 11 
     | 
    
         
            +
                @end_time = end_time
         
     | 
| 
      
 12 
     | 
    
         
            +
                @comment = comment
         
     | 
| 
      
 13 
     | 
    
         
            +
                @record_number = nil
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
              attr_accessor :project
         
     | 
| 
      
 18 
     | 
    
         
            +
              attr_reader :start_time
         
     | 
| 
      
 19 
     | 
    
         
            +
              attr_reader :end_time
         
     | 
| 
      
 20 
     | 
    
         
            +
              attr_accessor :comment
         
     | 
| 
      
 21 
     | 
    
         
            +
              attr_accessor :record_number
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
              def conflict?(other_entry)
         
     | 
| 
      
 24 
     | 
    
         
            +
                to_range.overlap? other_entry.to_range
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
              def start_time=(new_start_time)
         
     | 
| 
      
 28 
     | 
    
         
            +
                raise ArgumentError.new("Start time must come before end time") if new_start_time > @end_time
         
     | 
| 
      
 29 
     | 
    
         
            +
                @start_time = new_start_time
         
     | 
| 
      
 30 
     | 
    
         
            +
              end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
              def end_time=(new_end_time)
         
     | 
| 
      
 33 
     | 
    
         
            +
                raise ArgumentError.new("End time must come after start time") if @start_time > new_end_time
         
     | 
| 
      
 34 
     | 
    
         
            +
                @end_time = new_end_time
         
     | 
| 
      
 35 
     | 
    
         
            +
              end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
              def duration
         
     | 
| 
      
 38 
     | 
    
         
            +
                return 0 if @start_time == nil || @end_time == nil
         
     | 
| 
      
 39 
     | 
    
         
            +
                RichUnits::Duration.new(@end_time - @start_time)
         
     | 
| 
      
 40 
     | 
    
         
            +
              end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
              def <=>(other_time_entry)
         
     | 
| 
      
 43 
     | 
    
         
            +
                self.start_time <=> other_time_entry.start_time
         
     | 
| 
      
 44 
     | 
    
         
            +
              end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
              def to_range
         
     | 
| 
      
 47 
     | 
    
         
            +
                Range.new(@start_time, @end_time, true)
         
     | 
| 
      
 48 
     | 
    
         
            +
              end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,71 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class TimeLog
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
              def initialize(store)
         
     | 
| 
      
 4 
     | 
    
         
            +
                @store = store
         
     | 
| 
      
 5 
     | 
    
         
            +
                @store.transaction do
         
     | 
| 
      
 6 
     | 
    
         
            +
                  @store[:entries] ||= []
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @store[:entry_record_number] ||= 0
         
     | 
| 
      
 8 
     | 
    
         
            +
                end
         
     | 
| 
      
 9 
     | 
    
         
            +
              end
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
              def count
         
     | 
| 
      
 12 
     | 
    
         
            +
                @store.transaction do
         
     | 
| 
      
 13 
     | 
    
         
            +
                  @store[:entries].count
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
              end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
              def add(entry)
         
     | 
| 
      
 18 
     | 
    
         
            +
                @store.transaction do
         
     | 
| 
      
 19 
     | 
    
         
            +
                  raise ArgumentError.new("Cannot add an entry which conflicts with existing entries.") if @store[:entries].any? { |e| e.conflict? entry }
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @store[:entry_record_number] += 1
         
     | 
| 
      
 21 
     | 
    
         
            +
                  entry.record_number = @store[:entry_record_number]
         
     | 
| 
      
 22 
     | 
    
         
            +
                  @store[:entries] << entry
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
              end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
              def find(record_number)
         
     | 
| 
      
 27 
     | 
    
         
            +
                @store.transaction do
         
     | 
| 
      
 28 
     | 
    
         
            +
                  found_entries = @store[:entries].select{|e| e.record_number == record_number}
         
     | 
| 
      
 29 
     | 
    
         
            +
                  raise "Record number #{record_number} is not unique in the database." if found_entries.count > 1
         
     | 
| 
      
 30 
     | 
    
         
            +
                  raise ArgumentError.new("Cannot find an entry with record number #{record_number} to edit") if found_entries.count == 0
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                  found_entries.first
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
              end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
              def extract_entries(start_time, end_time)
         
     | 
| 
      
 37 
     | 
    
         
            +
                covered_entries = []
         
     | 
| 
      
 38 
     | 
    
         
            +
                @store.transaction do
         
     | 
| 
      
 39 
     | 
    
         
            +
                  span_to_cover = Range.new(start_time, end_time, true)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  covered_entries = @store[:entries].select{|e| span_to_cover.overlap? e.to_range }
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
                covered_entries
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
              def update(record_number, properties)
         
     | 
| 
      
 46 
     | 
    
         
            +
                @store.transaction do
         
     | 
| 
      
 47 
     | 
    
         
            +
                  found_entries = @store[:entries].select{|e| e.record_number == record_number}
         
     | 
| 
      
 48 
     | 
    
         
            +
                  raise "Record number #{record_number} is not unique in the database." if found_entries.count > 1
         
     | 
| 
      
 49 
     | 
    
         
            +
                  raise ArgumentError.new("Cannot find an entry with record number #{record_number} to edit") if found_entries.count == 0
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                  entry = found_entries.first
         
     | 
| 
      
 52 
     | 
    
         
            +
                  entry.project = properties[:project] if properties[:project]
         
     | 
| 
      
 53 
     | 
    
         
            +
                  entry.start_time = properties[:start] if properties[:start]
         
     | 
| 
      
 54 
     | 
    
         
            +
                  entry.end_time = properties[:end] if properties[:end]
         
     | 
| 
      
 55 
     | 
    
         
            +
                  entry.comment = properties[:comment] if properties[:comment]
         
     | 
| 
      
 56 
     | 
    
         
            +
                  raise ArgumentError.new("The new times for this entry conflict with existing entries.") if @store[:entries].any? { |e| e != entry && e.conflict?(entry) }
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
              end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
              def delete(record_number)
         
     | 
| 
      
 61 
     | 
    
         
            +
                @store.transaction do
         
     | 
| 
      
 62 
     | 
    
         
            +
                  found_entries = @store[:entries].select{|e| e.record_number == record_number}
         
     | 
| 
      
 63 
     | 
    
         
            +
                  raise "Record number #{record_number} is not unique in the database." if found_entries.count > 1
         
     | 
| 
      
 64 
     | 
    
         
            +
                  raise ArgumentError.new("Cannot find an entry with record number #{record_number} to delete.") if found_entries.count == 0
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                  @store[:entries].delete(found_entries.first)
         
     | 
| 
      
 67 
     | 
    
         
            +
                end
         
     | 
| 
      
 68 
     | 
    
         
            +
              end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
              
         
     | 
| 
      
 71 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,113 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'ruport'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'time'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            class TimeReport
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
              def initialize(entries)
         
     | 
| 
      
 7 
     | 
    
         
            +
                @entries = entries
         
     | 
| 
      
 8 
     | 
    
         
            +
              end
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
              attr_accessor :entries
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
              def trim(start_time, end_time)
         
     | 
| 
      
 13 
     | 
    
         
            +
                @entries.map! do |e|
         
     | 
| 
      
 14 
     | 
    
         
            +
                  e.start_time = start_time if e.to_range.include?(start_time)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  e.end_time = end_time if e.to_range.include?(end_time)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  e
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
              end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
              def report(command_options, outstream = STDOUT)
         
     | 
| 
      
 21 
     | 
    
         
            +
                trim(command_options[:start], command_options[:end])
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                detail_report(outstream) if command_options[:detail]
         
     | 
| 
      
 24 
     | 
    
         
            +
                summary_report(outstream) if command_options[:summary]
         
     | 
| 
      
 25 
     | 
    
         
            +
                byday_report(outstream) if command_options[:byday]
         
     | 
| 
      
 26 
     | 
    
         
            +
              end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
              private
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
              def detail_report(outstream)
         
     | 
| 
      
 31 
     | 
    
         
            +
               table = Ruport::Data::Table({ :column_names => ["Id", "Project", "Start - Stop", "Hours", "Comment"]}) do |table|
         
     | 
| 
      
 32 
     | 
    
         
            +
                  @entries.sort.each do |entry|
         
     | 
| 
      
 33 
     | 
    
         
            +
                    row = []
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                    # format record number
         
     | 
| 
      
 36 
     | 
    
         
            +
                    row << sprintf("%5d", entry.record_number)
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                    # format project
         
     | 
| 
      
 39 
     | 
    
         
            +
                    row << entry.project
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                    # format start - stop
         
     | 
| 
      
 42 
     | 
    
         
            +
                    str = ""
         
     | 
| 
      
 43 
     | 
    
         
            +
                    str << entry.start_time.strftime("%m/%d/%Y at %I:%M %p")
         
     | 
| 
      
 44 
     | 
    
         
            +
                    str << " to "
         
     | 
| 
      
 45 
     | 
    
         
            +
                    str << entry.end_time.strftime("%m/%d/%Y at ") if ( (entry.start_time.year != entry.end_time.year) && (entry.start_time.yday != entry.end_time.yday))
         
     | 
| 
      
 46 
     | 
    
         
            +
                    str << entry.end_time.strftime("%I:%M %p")
         
     | 
| 
      
 47 
     | 
    
         
            +
                    row << str
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                    # format duration
         
     | 
| 
      
 50 
     | 
    
         
            +
                    str = ""
         
     | 
| 
      
 51 
     | 
    
         
            +
                    str << entry.duration.strftime("%d Days ") if entry.duration.days > 0
         
     | 
| 
      
 52 
     | 
    
         
            +
                    str << entry.duration.strftime("%hh %mm")
         
     | 
| 
      
 53 
     | 
    
         
            +
                    row << str
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                    # format comment
         
     | 
| 
      
 56 
     | 
    
         
            +
                    row << entry.comment
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                    table << row
         
     | 
| 
      
 59 
     | 
    
         
            +
                  end
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                outstream.puts table.as(:text, {:ignore_table_width => true})
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
              end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
              def summary_report(outstream)
         
     | 
| 
      
 67 
     | 
    
         
            +
                summary = Hash.new(0)
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                total = @entries.inject(0) do |sum, entry|
         
     | 
| 
      
 70 
     | 
    
         
            +
                  summary[entry.project] += entry.duration.to_i
         
     | 
| 
      
 71 
     | 
    
         
            +
                  sum += entry.duration.to_i
         
     | 
| 
      
 72 
     | 
    
         
            +
                end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                width = summary.keys.max { |a, b| a.length <=> b.length }.length
         
     | 
| 
      
 75 
     | 
    
         
            +
                summary.sort.each do |key, value|
         
     | 
| 
      
 76 
     | 
    
         
            +
                  duration = RichUnits::Duration.new(value)
         
     | 
| 
      
 77 
     | 
    
         
            +
                  duration.reset_segments(:hours, :minutes)
         
     | 
| 
      
 78 
     | 
    
         
            +
                  outstream.puts "#{key.rjust(width)} #{duration.strftime("%hh").rjust(4)}#{duration.strftime("%mm").rjust(4)}"
         
     | 
| 
      
 79 
     | 
    
         
            +
                end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                total_duration = RichUnits::Duration.new(total)
         
     | 
| 
      
 82 
     | 
    
         
            +
                total_duration.reset_segments(:hours, :minutes)
         
     | 
| 
      
 83 
     | 
    
         
            +
                outstream.puts "-" * (width + 4 + 4 + 1)
         
     | 
| 
      
 84 
     | 
    
         
            +
                outstream.puts "#{"Total".rjust(width)} #{total_duration.strftime("%hh").rjust(4)}#{total_duration.strftime("%mm").rjust(4)}"
         
     | 
| 
      
 85 
     | 
    
         
            +
              end
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
              def byday_report(outstream)
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                hash = {}
         
     | 
| 
      
 90 
     | 
    
         
            +
                comments = {}
         
     | 
| 
      
 91 
     | 
    
         
            +
                @entries.each do |entry|
         
     | 
| 
      
 92 
     | 
    
         
            +
                  start_date = Date.new(entry.start_time.year, entry.start_time.month, entry.start_time.day)
         
     | 
| 
      
 93 
     | 
    
         
            +
                  hash[start_date] ||= Hash.new(0)
         
     | 
| 
      
 94 
     | 
    
         
            +
                  hash[start_date][entry.project] += entry.duration.to_i
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                  comments[start_date] ||= Hash.new { |h, k| h[k] = [] }
         
     | 
| 
      
 97 
     | 
    
         
            +
                  comments[start_date][entry.project] << entry.comment
         
     | 
| 
      
 98 
     | 
    
         
            +
                end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                hash.sort.each do |entry_date, project_hash|
         
     | 
| 
      
 101 
     | 
    
         
            +
                  outstream.puts entry_date.strftime("%D")
         
     | 
| 
      
 102 
     | 
    
         
            +
                  project_hash.sort.each do |project, duration|
         
     | 
| 
      
 103 
     | 
    
         
            +
                    project_time = RichUnits::Duration.new(duration)
         
     | 
| 
      
 104 
     | 
    
         
            +
                    project_time.reset_segments(:hours, :minutes)
         
     | 
| 
      
 105 
     | 
    
         
            +
                    outstream.puts "\t#{project} #{project_time.strftime("%hh %mm")}"
         
     | 
| 
      
 106 
     | 
    
         
            +
                    comments[entry_date][project].each do |comment|
         
     | 
| 
      
 107 
     | 
    
         
            +
                      outstream.puts "\t - #{comment}"
         
     | 
| 
      
 108 
     | 
    
         
            +
                    end
         
     | 
| 
      
 109 
     | 
    
         
            +
                  end
         
     | 
| 
      
 110 
     | 
    
         
            +
                end
         
     | 
| 
      
 111 
     | 
    
         
            +
              end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
            end
         
     |