timet 0.2.2 → 0.8.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5987e8625e55cc1ce0c9ceca0cdafc752d6299bb61cf25b910d5cf27997145fe
4
- data.tar.gz: dbcd75820a2f84d1870592d37ff698986e40b6b938bf57f598ca335a3bf592b8
3
+ metadata.gz: 85190a5b5ae6c446a96b2dda4c71efbe19ae0a1ec907dd5d699b1d831ecd0cd7
4
+ data.tar.gz: ecfd87a600e956b45703fc02865503dd739079f5d8b003770ee9dcb554338903
5
5
  SHA512:
6
- metadata.gz: 619fecc791710b66e6022dca17526b8a728ba6bad7da09053052dcd632321b5a3400af4b3fd5d477857aff9d04dee326f109cb84c5feac3ad13a8f8e550d7dbe
7
- data.tar.gz: e51aa5af43607e9176dd90b5f0eab61492117d97a517039684e38fe93de76fb6b81b642f550226ecaaa8b83e5708ae09bc77ba406da06a10d43230e81bf1645f
6
+ metadata.gz: 2866257d4d685e76cbd32874922f7bd837f7a68d59bd152e9575935f63dd98ac1514e5e08a1c019bf661ded1ed409ad6733a235194e996c1d50d4dec9494226c
7
+ data.tar.gz: a7e80d8c5e6f1d433b1549f98351721f5b6d28da242062c149cf47289f81ccb8c7808b2a13a3338550770c7594ad26ca4c91862e0da667c7f50259b6a10841cf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,88 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.8.1] - 2024-10-02
4
+ **Bug fixes:**
5
+ - Fixed a LoadError caused by the byebug gem being required after its removal.
6
+
7
+ ## [0.8.0] - 2024-10-01
8
+
9
+ **Improvements:**
10
+ - Introduced TimeHelper module to encapsulate time-related functionalities.
11
+ - Replaced direct calls to Time.now.to_i with TimeHelper.current_timestamp for consistency and modularity.
12
+ - Moved current_timestamp method to TimeHelper to centralize time-related logic.
13
+ - Ensured all timestamps are handled in UTC to avoid timezone issues.
14
+ - Refactored ValidationEditHelper to use TimeHelper for time-related operations.
15
+ - Simplified the conditional logic in update_time_field by using a single assignment with the || operator.
16
+ - Introduced local variables field_index and timestamp to store intermediate results, reducing redundancy and improving readability.
17
+ - Added ApplicationHelper module to handle common application tasks.
18
+ - Moved display and prompt logic for editing items into helper methods.
19
+ - Removed redundant field_value method and integrated its functionality into the helper methods.
20
+ - Consolidated the validation logic for start and end fields into a single conditional statement.
21
+ - Removed redundant validate_start_field and validate_end_field methods.
22
+ - Added a check to ensure the CSV export only occurs if there are items to export.
23
+ - Added a message to inform the user if no items are found for export.
24
+ - Refactored the validate_and_update method from Timet::Application to a more modular approach.
25
+ - Created new helper methods in ValidationEditHelper for fetching item values and validating start/end times.
26
+ - Enhanced readability and maintainability of the code by isolating concerns and reusing logic.
27
+ - Renamed item to last_item for clarity in the context where it represents the last task.
28
+ - Added a new variable last_item_status to explicitly define the status of the last item.
29
+ - Extracted table formatting logic into a new Formatter module.
30
+ - Removed redundant methods (format_table_header, format_table_separator, format_table_row, format_notes) from the TimeReport class.
31
+ - Introduced current_timestamp method to encapsulate the logic for getting the current timestamp.
32
+ - Added insert_item_if_valid method to handle item insertion based on valid statuses.
33
+ - Introduced VALID_STATUSES_FOR_INSERTION constant to define valid statuses for item insertion.
34
+ - Updated StatusHelper to use :in_progress status instead of :incomplete.
35
+ - Ensured consistency in timestamp handling across methods.
36
+ - Added support for editing notes.
37
+ - Added the csv gem and moved the extract_date method to the TimeHelper module.
38
+ - Improved error handling by ensuring the CSV filename is correctly processed and validated.
39
+ - Added a confirmation message when the CSV file is successfully exported.
40
+
41
+ **Bug fixes:**
42
+ - Updated timestamp_to_time method to return nil if the input timestamp is nil.
43
+ - Removed unnecessary &.then syntax for clarity and consistency.
44
+ - Corrected the test expectations to use the correct Unix timestamp.
45
+ - Updated format_time and timestamp_to_date methods to return nil if the input timestamp is nil.
46
+ - Ensured that the formatted time string does not include an extra space at the end.
47
+ - Updated update_time_field to handle nil values for the time field by using a conditional assignment.
48
+ - Ensured that the method correctly handles cases where the time field is not set, defaulting to the current timestamp.
49
+ - Addressed the NilCheck warning and improved the robustness of the method.
50
+ - Added validation to prevent updating start or end times with nil values.
51
+
52
+ **Tasks:**
53
+ - Added ENV['TZ'] = 'UTC' to spec_helper.rb to ensure that all RSpec tests run in the UTC time zone.
54
+ - Updated tests to reflect the new structure and ensure items are returned for export.
55
+ - Updated and added tests to cover the new export logic.
56
+ - Updated the expectations to use the renamed variables, ensuring the code remains clear and maintainable.
57
+ - Included the Formatter module in the TimeReport class to utilize its formatted output.
58
+ - Updated README.md with alternative syntax for 'timet start' command.
59
+ - Update README.md with new command reference and edit functionality
60
+ - Refactored start method tests to use context blocks for better readability and organization.
61
+ - Added tests to verify the behavior of the start method when the database is empty, the last item is complete, or the last item is still in progress.
62
+ - Added tests to ensure the start method handles notes provided via the --notes option correctly.
63
+ - Refactored stop method tests to use context blocks and added tests to verify the behavior when the last item is in progress or complete.
64
+ - Refactored resume method tests to use context blocks and added tests to verify the behavior when a task is currently being tracked, when there is a last task, and when there are no items.
65
+ - Refactored summary method tests to use context blocks and added tests to verify the behavior for different combinations of arguments.
66
+ - Updated cancel method test to reflect the correct status for active time tracking.
67
+ - Updated database_spec.rb to reflect the correct status for in-progress items.
68
+
69
+ **Additional Considerations:**
70
+ - Ensured that all timestamps are handled in UTC to avoid timezone issues across different environments.
71
+ - Updated the test to reflect the correct UTC time zone.
72
+ - Ensured consistency in timestamp handling across methods.
73
+ - Improved overall structure, readability, and maintainability of the code.
74
+ - Made the application easier to maintain and extend in the future.
75
+
76
+ **Refactor and enhance time formatting methods**
77
+ - Refactored `format_time_string` method in `TimeHelper` to improve readability and maintainability.
78
+ - Added detailed documentation for the `format_time_string` method.
79
+ - Simplified the logic for parsing and validating time components.
80
+ - Updated `ValidationEditHelper` to use the refactored `format_time_string` method.
81
+ - Added comprehensive RSpec tests for the `format_time_string` method to cover various input scenarios, including edge cases.
82
+ - Fixed a bug where `nil` input was not handled correctly.
83
+ - Ensured that invalid time values return `nil` instead of an empty string.
84
+ - Refactored the CSV export logic in `TimeReport` to improve readability and maintainability.
85
+
3
86
  ## [0.2.2] - 2024-09-27
4
87
 
5
88
  * **Improvements**:
data/README.md CHANGED
@@ -35,7 +35,7 @@ Old versions of Ruby and Sqlite:
35
35
 
36
36
  Install the gem by executing:
37
37
  ```bash
38
- $ gem install timet
38
+ gem install timet
39
39
  ```
40
40
 
41
41
  ## Usage
@@ -43,6 +43,10 @@ $ gem install timet
43
43
  - **timet start [tag] --notes='...'**: Starts tracking time for a task labeled with the provided [tag] and notes (optional). Example:
44
44
  ```bash
45
45
  timet start task1 --notes='Meeting with client'
46
+
47
+ or
48
+
49
+ timet start task1 'Meeting with client'
46
50
  ```
47
51
 
48
52
  ```
@@ -86,61 +90,44 @@ $ gem install timet
86
90
  +-------+------------+--------+----------+----------+----------+--------------------------+
87
91
  ```
88
92
 
89
- - **timet summary today (t)**: Display a report of tracked time for today.
90
-
91
- ```bash
92
- timet summary today
93
- ```
94
-
95
- - **timet summary yesterday (y)**: Display a report of tracked time for yesterday.
96
-
97
- ```bash
98
- timet summary yesterday
99
- ```
100
-
101
- - **timet summary week (w)**: Display a report of tracked time for the week.
102
-
103
- ```bash
104
- timet summary week
105
- ```
106
-
107
- - **timet summary resume (r)**: Resume tracking the last task.
108
-
109
- ```bash
110
- timet summary resume
111
- ```
112
-
113
- - **timet summary resume (r)**: Resume tracking the last month.
114
-
115
- ```bash
116
- timet summary month
117
- ```
118
-
119
- - **timet su t --csv=[filename]**: Display a report of tracked time for today and export it to filename.csv
120
-
121
- ```bash
122
- timet su t --csv=summary_today.csv
123
- ```
124
-
125
- - **timet delete [id]**: Delete a task
126
-
127
- ```bash
128
- timet delete [id]
129
-
130
- or
131
-
132
- timet d [id]
133
- ```
134
-
135
- - **timet cancel**: Cancel active time tracking
136
93
 
137
- ```bash
138
- timet cancel
94
+ - **timet edit**: It allows users update a task's notes, tag, start or end fields.
95
+ ```bash
96
+ timet e 1
97
+ ```
139
98
 
140
- or
99
+ ```
100
+ Tracked time report [today]:
101
+ +-------+------------+--------+----------+----------+----------+--------------------------+
102
+ | Id | Date | Tag | Start | End | Duration | Notes |
103
+ +-------+------------+--------+----------+----------+----------+--------------------------+
104
+ | 2 | 2024-08-09 | task1 | 16:15:07 | - | 00:00:00 | Meeting with client |
105
+ | 1 | | task1 | 14:55:07 | 15:55:07 | 01:00:00 | Meeting with client |
106
+ +-------+------------+--------+----------+----------+----------+--------------------------+
107
+ | Total: | 01:00:00 | |
108
+ +-------+------------+--------+----------+----------+----------+--------------------------+
109
+ Edit Field? (Press ↑/↓ arrow to move and Enter to select)
110
+ ‣ Notes
111
+ Tag
112
+ Start
113
+ End
114
+ ```
141
115
 
142
- timet c
143
- ```
116
+ ## Command Reference
117
+
118
+ | Command | Description | Example Usage |
119
+ |----------------------------------------------|-----------------------------------------------------------------------------|---------------------------------------------------|
120
+ | `timet start [tag] --notes='...'` | Start tracking time for a task labeled [tag] and notes (optional). | `timet start Task "My notes"` |
121
+ | `timet stop` | Stop tracking time. | `timet start Task "My notes"` |
122
+ | `timet summary today (t)` | Display a report of tracked time for today. | `timet su t` or `timet su` |
123
+ | `timet summary yesterday (y)` | Display a report of tracked time for yesterday. | `timet su y` |
124
+ | `timet summary week (w)` | Display a report of tracked time for the week. | `timet su w` |
125
+ | `timet summary month (m)` | Resume tracking the last month. | `timet su m` |
126
+ | `timet su t --csv=[filename]` | Display a report of tracked time for today and export it to `filename.csv`. | `timet su t --csv=file.csv` |
127
+ | `timet summary resume (r)` | Resume tracking the last task. | `timet su r` |
128
+ | `timet delete [id]` | Delete a task by its ID. | `timet d [id]` |
129
+ | `timet cancel` | Cancel active time tracking. | `timet c` |
130
+ | `timet edit [id]` | Update a task's notes, tag, start or end fields. | `timet e [1]` |
144
131
 
145
132
 
146
133
  ## Data
@@ -3,29 +3,51 @@
3
3
  require_relative 'version'
4
4
  require 'thor'
5
5
  require 'tty-prompt'
6
- require 'byebug'
6
+ require_relative 'validation_edit_helper'
7
+ require_relative 'application_helper'
8
+ require_relative 'time_helper'
7
9
 
8
10
  module Timet
9
- # Tracks time spent on various tasks.
11
+ # Application class that defines CLI commands for time tracking:
12
+ # - start: Start time tracking with optional notes
13
+ # - stop: Stop time tracking
14
+ # - resume: Resume the last task
15
+ # - summary: Display a summary of tracked time and export to CSV
16
+ # - edit: Edit a task
17
+ # - delete: Delete a task
18
+ # - cancel: Cancel active time tracking
10
19
  class Application < Thor
20
+ include ValidationEditHelper
21
+ include ApplicationHelper
22
+
11
23
  def initialize(*args)
12
24
  super
13
25
  @db = Timet::Database.new
14
26
  end
15
27
 
28
+ FIELD_INDEX = {
29
+ 'notes' => 4,
30
+ 'tag' => 3,
31
+ 'start' => 1,
32
+ 'end' => 2
33
+ }.freeze
34
+
35
+ VALID_STATUSES_FOR_INSERTION = %i[no_items complete].freeze
36
+
16
37
  desc "start [tag] --notes='...'", "start time tracking --notes='my notes...'"
17
38
  option :notes, type: :string, desc: 'Add a note'
18
39
  def start(tag, notes = nil)
19
- start = Time.now.to_i
40
+ start_time = TimeHelper.current_timestamp
20
41
  notes = options[:notes] || notes
21
- @db.insert_item(start, tag, notes) if %i[no_items complete].include?(@db.last_item_status)
42
+
43
+ insert_item_if_valid(start_time, tag, notes)
22
44
  summary
23
45
  end
24
46
 
25
47
  desc 'stop', 'stop time tracking'
26
48
  def stop
27
- stop = Time.now.to_i
28
- @db.update(stop) if @db.last_item_status == :incomplete
49
+ stop = TimeHelper.current_timestamp
50
+ @db.update(stop) if @db.last_item_status == :in_progress
29
51
  result = @db.last_item
30
52
 
31
53
  return unless result
@@ -35,12 +57,18 @@ module Timet
35
57
 
36
58
  desc 'resume (r)', 'resume last task'
37
59
  def resume
38
- if @db.last_item_status == :incomplete
60
+ status = @db.last_item_status
61
+
62
+ case status
63
+ when :in_progress
39
64
  puts 'A task is currently being tracked.'
40
- elsif @db.last_item.any?
41
- tag = @db.last_item[3]
42
- notes = @db.last_item[4]
43
- start(tag, notes)
65
+ when :complete
66
+ last_item = @db.last_item
67
+ if last_item
68
+ tag = last_item[FIELD_INDEX['tag']]
69
+ notes = last_item[FIELD_INDEX['notes']]
70
+ start(tag, notes)
71
+ end
44
72
  end
45
73
  end
46
74
 
@@ -49,10 +77,27 @@ module Timet
49
77
  and export to csv_filename"
50
78
  option :csv, type: :string, desc: 'Export to CSV file'
51
79
  def summary(filter = nil, tag = nil)
52
- csv_filename = options[:csv]
80
+ csv_filename = options[:csv].split('.')[0] if options[:csv]
53
81
  summary = TimeReport.new(@db, filter, tag, csv_filename)
54
82
  summary.display
55
- summary.export_sheet if csv_filename
83
+ if csv_filename && summary.items.any?
84
+ summary.export_sheet
85
+ elsif summary.items.empty?
86
+ puts 'No items found to export'
87
+ end
88
+ end
89
+
90
+ desc 'edit (e) [id]', 'edit a task'
91
+ def edit(id)
92
+ item = @db.find_item(id)
93
+ return puts "No tracked time found for id: #{id}" unless item
94
+
95
+ display_item(item)
96
+ field = select_field_to_edit
97
+ new_value = prompt_for_new_value(item, field)
98
+ validate_and_update(item, field, new_value)
99
+
100
+ summary.display
56
101
  end
57
102
 
58
103
  desc 'delete (d) [id]', 'delete a task'
@@ -60,7 +105,7 @@ module Timet
60
105
  item = @db.find_item(id)
61
106
  return puts "No tracked time found for id: #{id}" unless item
62
107
 
63
- TimeReport.new(@db, nil, nil, nil).show_row(item)
108
+ TimeReport.new(@db).show_row(item)
64
109
  return unless TTY::Prompt.new.yes?('Are you sure you want to delete this entry?')
65
110
 
66
111
  delete_item_and_print_message(id, "Deleted #{id}")
@@ -80,6 +125,12 @@ module Timet
80
125
 
81
126
  private
82
127
 
128
+ def insert_item_if_valid(start_time, tag, notes)
129
+ return unless VALID_STATUSES_FOR_INSERTION.include?(@db.last_item_status)
130
+
131
+ @db.insert_item(start_time, tag, notes)
132
+ end
133
+
83
134
  def delete_item_and_print_message(id, message)
84
135
  @db.delete_item(id)
85
136
  puts message
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Timet
4
+ # Provides helper methods for the Timet application.
5
+ module ApplicationHelper
6
+ def display_item(item)
7
+ TimeReport.new(@db).show_row(item)
8
+ end
9
+
10
+ def prompt_for_new_value(item, field)
11
+ current_value = field_value(item, field)
12
+ prompt = TTY::Prompt.new(active_color: :green)
13
+ prompt.ask("Update #{field} (#{current_value}):")
14
+ end
15
+
16
+ def select_field_to_edit
17
+ prompt = TTY::Prompt.new(active_color: :green)
18
+ prompt.select('Edit Field?', Timet::Application::FIELD_INDEX.keys.map(&:capitalize), active_color: :cyan).downcase
19
+ end
20
+
21
+ def field_value(item, field)
22
+ index = Timet::Application::FIELD_INDEX[field]
23
+ value = item[index]
24
+ return TimeHelper.timestamp_to_time(value) if %w[start end].include?(field)
25
+
26
+ value
27
+ end
28
+ end
29
+ end
@@ -54,6 +54,12 @@ module Timet
54
54
  execute_sql("DELETE FROM items WHERE id = #{id}")
55
55
  end
56
56
 
57
+ def update_item(id, field, value)
58
+ return if %w[start end].include?(field) && value.nil?
59
+
60
+ execute_sql("UPDATE items SET #{field}='#{value}' WHERE id = #{id}")
61
+ end
62
+
57
63
  # Fetches the ID of the last inserted item
58
64
  def fetch_last_id
59
65
  result = execute_sql('SELECT id FROM items ORDER BY id DESC LIMIT 1').first
@@ -81,7 +87,7 @@ module Timet
81
87
  start_time = last_item[1]
82
88
  end_time = last_item[2]
83
89
 
84
- total_seconds = end_time ? end_time - start_time : Time.now.to_i - start_time
90
+ total_seconds = end_time ? end_time - start_time : TimeHelper.current_timestamp - start_time
85
91
  seconds_to_hms(total_seconds)
86
92
  end
87
93
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Timet
4
+ # This module is responsible for formatting the output of the `timet` application.
5
+ # It provides methods for formatting the table header, separators, and rows.
6
+ module Formatter
7
+ def format_table_header
8
+ header = <<~TABLE
9
+ Tracked time report \u001b[31m[#{@filter}]\033[0m:
10
+ #{format_table_separator}
11
+ \033[32m| Id | Date | Tag | Start | End | Duration | Notes |\033[0m
12
+ #{format_table_separator}
13
+ TABLE
14
+ puts header
15
+ end
16
+
17
+ def format_table_separator
18
+ '+-------+------------+--------+----------+----------+----------+--------------------------+'
19
+ end
20
+
21
+ def format_table_row(*row)
22
+ id, tag, start_date, start_time, end_time, duration, notes = row
23
+ "| #{id.to_s.rjust(5)} | #{start_date} | #{tag.ljust(6)} | #{start_time.split[1]} | " \
24
+ "#{end_time.split[1].rjust(8)} | #{@db.seconds_to_hms(duration).rjust(8)} | #{format_notes(notes)} |"
25
+ end
26
+
27
+ def format_notes(notes)
28
+ return ' ' * 23 if notes.nil?
29
+
30
+ notes = "#{notes.slice(0, 20)}..." if notes.length > 20
31
+ notes.ljust(23)
32
+ end
33
+ end
34
+ end
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Determines the status of a time tracking result based on the presence and end time of items.
4
- module StatusHelper
5
- def self.determine_status(result)
6
- return :no_items if result.empty?
3
+ module Timet
4
+ # Determines the status of a time tracking result based on the presence and end time of items.
5
+ module StatusHelper
6
+ def self.determine_status(result)
7
+ return :no_items if result.empty?
7
8
 
8
- last_item_end = result.first[1]
9
- return :incomplete unless last_item_end
9
+ last_item_end = result.first[1]
10
+ return :in_progress unless last_item_end
10
11
 
11
- :complete
12
+ :complete
13
+ end
12
14
  end
13
15
  end
@@ -1,36 +1,96 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This module provides helper functions for working with time and dates.
4
- # It includes methods for:
5
- # - formatting timestamps into a specific format
6
- # - calculating the duration between two timestamps
7
- # - converting a Date object to a timestamp
8
- module TimeHelper
9
- def self.format_time(timestamp)
10
- timestamp&.then { |time| Time.at(time).strftime('%Y-%m-%d %H:%M:%S').ljust(19) }
11
- end
3
+ module Timet
4
+ # This module provides helper functions for working with time and dates.
5
+ # It includes methods for:
6
+ # - formatting timestamps into a specific format
7
+ # - calculating the duration between two timestamps
8
+ # - converting a Date object to a timestamp
9
+ module TimeHelper
10
+ def self.format_time(timestamp)
11
+ return nil if timestamp.nil?
12
12
 
13
- def self.timestamp_to_date(timestamp)
14
- timestamp&.then { |time| Time.at(time).strftime('%Y-%m-%d') }
15
- end
13
+ Time.at(timestamp).strftime('%Y-%m-%d %H:%M:%S')
14
+ end
16
15
 
17
- def self.timestamp_to_time(timestamp)
18
- timestamp&.then { |time| Time.at(time).strftime('%H:%M:%S') }
19
- end
16
+ def self.timestamp_to_date(timestamp)
17
+ return nil if timestamp.nil?
20
18
 
21
- def self.calculate_duration(start_time, end_time)
22
- start_time = Time.at(start_time)
23
- end_time = end_time ? Time.at(end_time) : Time.now
19
+ Time.at(timestamp).strftime('%Y-%m-%d')
20
+ end
24
21
 
25
- (end_time - start_time).to_i
26
- end
22
+ def self.timestamp_to_time(timestamp)
23
+ return nil if timestamp.nil?
27
24
 
28
- def self.date_to_timestamp(date)
29
- date.to_time.to_i
30
- end
25
+ Time.at(timestamp).strftime('%H:%M:%S')
26
+ end
27
+
28
+ def self.calculate_duration(start_time, end_time)
29
+ end_time = end_time ? Time.at(end_time) : current_timestamp
30
+ (end_time - start_time).to_i
31
+ end
32
+
33
+ def self.date_to_timestamp(date)
34
+ date.to_time.to_i
35
+ end
36
+
37
+ def self.calculate_end_time(start_date, end_date)
38
+ end_date ||= start_date + 1
39
+ date_to_timestamp(end_date)
40
+ end
41
+
42
+ def self.extract_date(items, idx)
43
+ current_start_date = items[idx][1]
44
+ date = TimeHelper.timestamp_to_date(current_start_date)
45
+
46
+ last_start_date = items[idx - 1][1] if idx.positive?
47
+ date if idx.zero? || date != TimeHelper.timestamp_to_date(last_start_date)
48
+ end
49
+
50
+ # Formats a time string into a standard HH:MM:SS format.
51
+ #
52
+ # @param input [String] The input string to format.
53
+ # @return [String] The formatted time string in HH:MM:SS format, or nil if the input is invalid.
54
+ #
55
+ # @example
56
+ # TimeHelper.format_time_string('123456') # => "12:34:56"
57
+ # TimeHelper.format_time_string('1234567') # => "12:34:56"
58
+ # TimeHelper.format_time_string('1234') # => "12:34:00"
59
+ # TimeHelper.format_time_string('123') # => "12:30:00"
60
+ # TimeHelper.format_time_string('12') # => "12:00:00"
61
+ # TimeHelper.format_time_string('1') # => "01:00:00"
62
+ # TimeHelper.format_time_string('127122') # => nil
63
+ # TimeHelper.format_time_string('abc') # => nil
64
+ def self.format_time_string(input)
65
+ return nil if input.nil? || input.empty?
66
+
67
+ digits = input.gsub(/\D/, '')[0..5]
68
+ return nil if digits.empty?
69
+
70
+ hours, minutes, seconds = parse_time_components(digits)
71
+ return nil unless valid_time?(hours, minutes, seconds)
72
+
73
+ format('%<hours>02d:%<minutes>02d:%<seconds>02d', hours: hours, minutes: minutes, seconds: seconds)
74
+ end
75
+
76
+ def self.parse_time_components(digits)
77
+ padded_digits = case digits.size
78
+ when 1 then "0#{digits}0000"
79
+ when 2 then "#{digits}0000"
80
+ when 3 then "#{digits}000"
81
+ when 4 then "#{digits}00"
82
+ else digits.ljust(6, '0')
83
+ end
84
+
85
+ padded_digits.scan(/.{2}/).map(&:to_i)
86
+ end
87
+
88
+ def self.valid_time?(hours, minutes, seconds)
89
+ hours < 24 && minutes < 60 && seconds < 60
90
+ end
31
91
 
32
- def self.calculate_end_time(start_date, end_date)
33
- end_date ||= start_date + 1
34
- date_to_timestamp(end_date)
92
+ def self.current_timestamp
93
+ Time.now.utc.to_i
94
+ end
35
95
  end
36
96
  end
@@ -2,17 +2,19 @@
2
2
 
3
3
  require 'date'
4
4
  require 'csv'
5
- require_relative 'time_helper'
6
5
  require_relative 'status_helper'
6
+ require_relative 'formatter'
7
7
 
8
8
  module Timet
9
9
  # The TimeReport class is responsible for displaying a report of tracked time
10
- # entries. It allows filtering the report by time periods (today, yesterday,
11
- # week) and displays a formatted table with the relevant information.
10
+ # entries. It allows filtering the report by time periods and displays
11
+ # a formatted table with the relevant information.
12
12
  class TimeReport
13
+ include Formatter
14
+
13
15
  attr_reader :db, :items, :filename
14
16
 
15
- def initialize(db, filter, tag, csv)
17
+ def initialize(db, filter = nil, tag = nil, csv = nil)
16
18
  @db = db
17
19
  @filename = csv
18
20
  @filter = formatted_filter(filter)
@@ -24,7 +26,7 @@ module Timet
24
26
 
25
27
  format_table_header
26
28
  items.each_with_index do |item, idx|
27
- date = extract_date(items, idx)
29
+ date = TimeHelper.extract_date(items, idx)
28
30
  display_time_entry(item, date)
29
31
  end
30
32
  puts format_table_separator
@@ -39,29 +41,32 @@ module Timet
39
41
  end
40
42
 
41
43
  def export_sheet
42
- CSV.open("#{filename}.csv", 'w') do |csv|
43
- csv << %w[ID Start End Tag Notes]
44
+ file_name = "#{filename}.csv"
45
+ write_csv(file_name)
44
46
 
45
- items.each do |id, start_time, end_time, tags, notes|
46
- csv << [
47
- id,
48
- TimeHelper.format_time(start_time),
49
- TimeHelper.format_time(end_time),
50
- tags,
51
- notes
52
- ]
53
- end
54
- end
47
+ puts "The #{file_name} has been exported."
55
48
  end
56
49
 
57
50
  private
58
51
 
59
- def extract_date(items, idx)
60
- current_start_date = items[idx][1]
61
- date = TimeHelper.timestamp_to_date(current_start_date)
52
+ def write_csv(file_name)
53
+ CSV.open(file_name, 'w') do |csv|
54
+ csv << %w[ID Start End Tag Notes]
55
+ items.each do |item|
56
+ csv << format_item(item)
57
+ end
58
+ end
59
+ end
62
60
 
63
- last_start_date = items[idx - 1][1]
64
- date if idx.zero? || date != TimeHelper.timestamp_to_date(last_start_date)
61
+ def format_item(item)
62
+ id, start_time, end_time, tags, notes = item
63
+ [
64
+ id,
65
+ TimeHelper.format_time(start_time),
66
+ TimeHelper.format_time(end_time),
67
+ tags,
68
+ notes
69
+ ]
65
70
  end
66
71
 
67
72
  def display_time_entry(item, date = nil)
@@ -71,7 +76,7 @@ module Timet
71
76
  duration = TimeHelper.calculate_duration(start_time_value, end_time_value)
72
77
  start_time = TimeHelper.format_time(start_time_value)
73
78
  end_time = TimeHelper.format_time(end_time_value) || '- -'
74
- start_date = date.nil? ? ' ' * 10 : date
79
+ start_date = date || (' ' * 10)
75
80
  puts format_table_row(id, tag_name[0..5], start_date, start_time, end_time, duration, notes)
76
81
  end
77
82
 
@@ -83,43 +88,26 @@ module Timet
83
88
  puts format_table_separator
84
89
  end
85
90
 
86
- def format_table_header
87
- header = <<~TABLE
88
- Tracked time report \u001b[31m[#{@filter}]\033[0m:
89
- #{format_table_separator}
90
- \033[32m| Id | Date | Tag | Start | End | Duration | Notes |\033[0m
91
- #{format_table_separator}
92
- TABLE
93
- puts header
94
- end
95
-
96
- def format_table_separator
97
- '+-------+------------+--------+----------+----------+----------+--------------------------+'
98
- end
99
-
100
- def format_table_row(*row)
101
- id, tag, start_date, start_time, end_time, duration, notes = row
102
- "| #{id.to_s.rjust(5)} | #{start_date} | #{tag.ljust(6)} | #{start_time.split[1]} | " \
103
- "#{end_time.split[1].rjust(8)} | #{@db.seconds_to_hms(duration).rjust(8)} | #{format_notes(notes)} |"
104
- end
105
-
106
91
  def filter_items(filter, tag)
107
- today = Date.today
108
- case filter
109
- when 'today'
110
- filter_by_date_range(today, nil, tag)
111
- when 'yesterday'
112
- filter_by_date_range(today - 1, nil, tag)
113
- when 'week'
114
- filter_by_date_range(today - 7, today + 1, tag)
115
- when 'month'
116
- filter_by_date_range(today - 30, today + 1, tag)
92
+ if date_ranges.key?(filter)
93
+ start_date, end_date = date_ranges[filter]
94
+ filter_by_date_range(start_date, end_date, tag)
117
95
  else
118
- puts 'Invalid filter. Supported filters: today, yesterday, week'
96
+ puts 'Invalid filter. Supported filters: today, yesterday, week, month'
119
97
  []
120
98
  end
121
99
  end
122
100
 
101
+ def date_ranges
102
+ today = Date.today
103
+ {
104
+ 'today' => [today, nil],
105
+ 'yesterday' => [today - 1, nil],
106
+ 'week' => [today - 7, today + 1],
107
+ 'month' => [today - 30, today + 1]
108
+ }
109
+ end
110
+
123
111
  def filter_by_date_range(start_date, end_date = nil, tag = nil)
124
112
  start_time = TimeHelper.date_to_timestamp(start_date)
125
113
  end_time = TimeHelper.calculate_end_time(start_date, end_date)
@@ -129,13 +117,6 @@ module Timet
129
117
  )
130
118
  end
131
119
 
132
- def format_notes(notes)
133
- return ' ' * 23 if notes.nil?
134
-
135
- notes = "#{notes.slice(0, 20)}..." if notes.length > 20
136
- notes.ljust(23)
137
- end
138
-
139
120
  def formatted_filter(filter)
140
121
  return 'today' if %w[today t].include?(filter)
141
122
  return 'yesterday' if %w[yesterday y].include?(filter)
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'time_helper'
4
+ module Timet
5
+ # Validates and updates a specific field of an item based on certain conditions.
6
+ # If the field is 'start' or 'end', it checks and updates the value accordingly.
7
+ # Otherwise, it directly updates the field with the new value.
8
+ module ValidationEditHelper
9
+ TIME_FIELDS = %w[start end].freeze
10
+
11
+ def validate_and_update(item, field, new_value)
12
+ return if new_value.nil?
13
+
14
+ id = item[0]
15
+
16
+ if TIME_FIELDS.include?(field)
17
+ process_and_update_time_field(item, field, new_value, id)
18
+ else
19
+ @db.update_item(id, field, new_value)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def process_and_update_time_field(item, field, date_value, id)
26
+ formatted_date = TimeHelper.format_time_string(date_value)
27
+
28
+ return print_error(date_value) unless formatted_date
29
+
30
+ new_date = update_time_field(item, field, formatted_date)
31
+ new_value_epoch = new_date.to_i
32
+
33
+ if valid_time_value?(item, field, new_value_epoch, id)
34
+ @db.update_item(id, field, new_value_epoch)
35
+ else
36
+ print_error(new_date)
37
+ end
38
+ end
39
+
40
+ def print_error(message)
41
+ puts "\u001b[31mInvalid date: #{message}\033[0m"
42
+ end
43
+
44
+ def update_time_field(item, field, formatted_value)
45
+ field_index = Timet::Application::FIELD_INDEX[field]
46
+ timestamp = item[field_index]
47
+ current_time = Time.at(timestamp || TimeHelper.current_timestamp).to_s.split
48
+ current_time[1] = formatted_value
49
+ DateTime.strptime(current_time.join(' '), '%Y-%m-%d %H:%M:%S %z').to_time
50
+ end
51
+
52
+ def valid_time_value?(item, field, new_value_epoch, id)
53
+ item_start = fetch_item_start(item)
54
+ item_end = fetch_item_end(item)
55
+ item_before_end = fetch_item_before_end(id, item_start)
56
+ item_after_start = fetch_item_after_start(id)
57
+
58
+ if field == 'start'
59
+ new_value_epoch >= item_before_end && new_value_epoch <= item_end
60
+ else
61
+ new_value_epoch >= item_start && new_value_epoch <= item_after_start
62
+ end
63
+ end
64
+
65
+ def fetch_item_start(item)
66
+ item[Timet::Application::FIELD_INDEX['start']]
67
+ end
68
+
69
+ def fetch_item_end(item)
70
+ item[Timet::Application::FIELD_INDEX['end']] || TimeHelper.current_timestamp
71
+ end
72
+
73
+ def fetch_item_before_end(id, item_start)
74
+ @db.find_item(id - 1)&.dig(Timet::Application::FIELD_INDEX['end']) || item_start
75
+ end
76
+
77
+ def fetch_item_after_start(id)
78
+ @db.find_item(id + 1)&.dig(Timet::Application::FIELD_INDEX['start']) || TimeHelper.current_timestamp
79
+ end
80
+ end
81
+ end
data/lib/timet/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Timet
4
- VERSION = '0.2.2'
4
+ VERSION = '0.8.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Vielma
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-27 00:00:00.000000000 Z
11
+ date: 2024-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -58,8 +58,7 @@ dependencies:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
60
  version: '1.7'
61
- description: It's a time tracking app that keeps track of how long you spend doing
62
- different things.
61
+ description: Timet is a command-line time tracker that keeps track of your activities.
63
62
  email:
64
63
  - frankvielma@gmail.com
65
64
  executables:
@@ -78,10 +77,13 @@ files:
78
77
  - bin/timet
79
78
  - lib/timet.rb
80
79
  - lib/timet/application.rb
80
+ - lib/timet/application_helper.rb
81
81
  - lib/timet/database.rb
82
+ - lib/timet/formatter.rb
82
83
  - lib/timet/status_helper.rb
83
84
  - lib/timet/time_helper.rb
84
85
  - lib/timet/time_report.rb
86
+ - lib/timet/validation_edit_helper.rb
85
87
  - lib/timet/version.rb
86
88
  - sig/timet.rbs
87
89
  homepage: https://frankvielma.github.io/posts/timet-a-powerful-command-line-tool-for-tracking-your-time/
@@ -110,5 +112,5 @@ requirements: []
110
112
  rubygems_version: 3.5.9
111
113
  signing_key:
112
114
  specification_version: 4
113
- summary: Command line time tracking with reports
115
+ summary: Command line time tracker with reports
114
116
  test_files: []