timet 1.2.0 → 1.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3f1c37d739d4c6712cf21af39357a45ded45b8a63d8d7e9932ac859fe72b000
4
- data.tar.gz: 20bf38e7554d3eb1326d1d7e2ae1095792c9b20a473d383f9f123e2ac0bbd2b2
3
+ metadata.gz: 822542486f30f170aa48dccd0577637caed0010047b085022653f3ae4d0cb848
4
+ data.tar.gz: 96b166cdd9ac9fe69ee546fec4d68e47d113df05f986b4aff61cc71358c1c9ea
5
5
  SHA512:
6
- metadata.gz: 92892c05063d444a713eaafec9eb63ff970a4926364b594dd31b61f229e9b415d3a171da61ffaa96660512708076423348ece7f120c0e21e075aa777e8e5b1b4
7
- data.tar.gz: 3e1799d361eb28b500ce81740eb9ee1f16d55048e9425203aa6482265a9bc1d891e6d6897c626b454b2a18854523b99ed4acb0edf3a63e0dc043f16d1ebf4b04
6
+ metadata.gz: 0c950e2ff135ba0f665a70960c0389a55ac83ccf863dbdcee219d83d4fbc1939b79411c6e549e082f8b0abc1e9df223079dc5f784bf466f18a8ec9a7d52be039
7
+ data.tar.gz: 4d76b845a5b3af8ad142e8a1edb65309f9b637b755af129401f7fdcfcc6635c1deabb37665302649fc3eaa3f4cf73f2212114111bc594dbdf6e62b5d1956c7ed
data/CHANGELOG.md CHANGED
@@ -1,5 +1,69 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.0] - 2024-10-22
4
+
5
+ **Improvements:**
6
+
7
+ - **Refactor `TimeReport` to use `TimeReportHelper` module for utility methods:**
8
+
9
+ - Extracted utility methods (`add_hashes`, `date_ranges`, `format_item`, `valid_date_format?`) into a new `TimeReportHelper` module.
10
+ - Updated `TimeReport` class to include `TimeReportHelper` module.
11
+ - Removed redundant utility methods from `TimeReport` class.
12
+ - Updated `display` method to use `process_time_entries` from `TimeReportHelper`.
13
+ - Updated `write_csv` method to use `write_csv_rows` from `TimeReportHelper`.
14
+ - Updated `print_time_block_chart` method to pass `colors` parameter to `format_tag_distribution`.
15
+ - Adjusted formatting in `total` method for better alignment.
16
+
17
+ - **Refactor `Timet::Formatter` to improve readability and modularity:**
18
+
19
+ - Introduced a constant `CHAR_MAPPING` to store block characters for different value ranges.
20
+ - Refactored `format_notes` method to use a more descriptive variable name for the maximum length.
21
+ - Updated `format_tag_distribution` method to accept `colors` parameter and pass it to `process_and_print_tags`.
22
+ - Extracted the logic for calculating `value` and `bar_length` into a separate method `calculate_value_and_bar_length`.
23
+ - Refactored `process_and_print_tags` to accept `colors` parameter and use the new `calculate_value_and_bar_length` method.
24
+ - Updated `print_time_block_chart` method to accept `colors` parameter and pass it to `print_blocks`.
25
+ - Refactored `print_blocks` method to accept `colors` and `start_time` parameters and use the new `print_time_blocks` method.
26
+ - Introduced `print_time_blocks` method to handle the printing of time blocks for each hour from the start time to 23.
27
+ - Introduced `get_formatted_block_char` method to retrieve the formatted block character and its associated tag for a given hour.
28
+ - Refactored `print_colored_block` method to use the `block` variable for clarity.
29
+ - Updated `get_block_char` method to use the `CHAR_MAPPING` constant for determining the block character.
30
+
31
+ - **Refactor `TimeHelper` methods and add new functionality:**
32
+ - Simplified nil checks in `format_time`, `timestamp_to_date`, and `timestamp_to_time` methods by using `unless` instead of `if`.
33
+ - Extracted the logic for calculating block end time and seconds into a new method `calculate_block_end_time_and_seconds`.
34
+ - Updated `count_seconds_per_hour_block` to use the new `calculate_block_end_time_and_seconds` method.
35
+ - Added a new method `append_tag_to_hour_blocks` to append a tag to each value in the `hour_blocks` hash.
36
+ - Removed the `aggregate_hash_values` method as it is no longer needed.
37
+ - Updated YARD documentation for all methods to reflect the changes.
38
+
39
+ **Bug fixes:**
40
+
41
+ - [ ] No bug fixes in this PR.
42
+
43
+ **Tasks:**
44
+
45
+ - Update `README.md` to reflect the changes.
46
+ - Update `Gemfile` and version to reflect the latest changes.
47
+
48
+ ## [1.2.1] - 2024-10-18
49
+
50
+ **Improvements:**
51
+
52
+ - Updated the time block chart formatting to use square brackets for better visual representation.
53
+ - Refactored the `play_sound_and_notify` method to avoid redundant platform checks and introduced platform-specific session runners.
54
+ - Improved readability and maintainability of the `format_tag_distribution` method by extracting logic into a new private method.
55
+ - Updated the `rubocop` gem from `~> 1.65` to `~> 1.67`.
56
+
57
+ ### Bug fixes:
58
+
59
+ - Fixed a `NoMethodError` caused by an undefined method `process_and_print_tags` in the `format_tag_distribution` method.
60
+ - Fixed line length violations in several files to comply with `rubocop` rules.
61
+
62
+ ### Additional Considerations:
63
+
64
+ - The changes in this pull request should be thoroughly tested to ensure that they do not introduce any regressions.
65
+ - Future improvements could include further refactoring to extract more logic into separate methods or classes, depending on the complexity and requirements of the application.
66
+
3
67
  ## [1.2.0] - 2024-10-11
4
68
 
5
69
  **Improvements:**
@@ -27,12 +91,32 @@
27
91
  - Integrated new methods into `time_report` to enhance time tracking visualization.
28
92
  - Updated the version number in `lib/timet/version.rb` to 1.2.0.
29
93
 
30
- ### Additional Considerations:
94
+ **Additional Considerations:**
31
95
 
32
96
  - The enhancements made in this pull request aim to improve the user experience and provide more powerful visualization and reporting capabilities for time tracking.
33
97
  - Reviewers are encouraged to test the new visualization methods and provide feedback on their effectiveness and usability.
34
98
  - The README updates should make it easier for new users to understand and use the `timet` tool.
35
99
 
100
+ ## [1.1.0] - 2024-10-09
101
+
102
+ **Improvements:**
103
+
104
+ - Added a new `version` command to display the current version of the Timet gem.
105
+ - Introduced an alias `tt` for the `timet` command, providing a shorter alternative.
106
+ - Updated the README to include the `tt` alias and provide examples for both `timet` and `tt` commands.
107
+ - Updated the gem version to `1.1.0`.
108
+ - Added the `tt` executable to the gemspec.
109
+ - Updated the `rspec-mocks` dependency to version `3.13.2`.
110
+
111
+ **Tasks:**
112
+
113
+ - Update `Gemfile.lock` to reflect the new gem version and updated dependencies.
114
+ - Add the `tt` executable script to the `bin` directory.
115
+ - Update the `version` command in `lib/timet/application.rb` with Yardoc documentation.
116
+ - Update the `VERSION` constant in `lib/timet/version.rb` to `1.1.0`.
117
+ - Update the `timet.gemspec` to include the `tt` executable.
118
+ - Update the README to reflect the new `tt` alias and provide examples for both `timet` and `tt` commands.
119
+
36
120
  ## [1.0.0] - 2024-10-07
37
121
 
38
122
  **Improvements:**
data/README.md CHANGED
@@ -19,7 +19,7 @@ Timet refers to a command-line tool designed to track your activities by recordi
19
19
  - **Querying and Reporting:** Generate detailed reports for specific periods.
20
20
  - **CSV Export:** Easily export your time tracking data to CSV format for further analysis or sharing.
21
21
  - **Pomodoro Integration:** The pomodoro option in the start command enhances time tracking by integrating the Pomodoro Technique.
22
- - **Block Time Plot:** Visualizes the distribution of tracked time across a 24-hour period, with bars in each column representing the amount of time tracked during that specific hour.
22
+ - **Block Time Plot:** Visualizes the distribution of tracked time across a specified range of dates, with bars in each column representing the amount of time tracked during that specific hour. The plot includes a header showing the hours and a row for each date, displaying the time blocks for each hour.
23
23
  - **Tag Distribution Plot:** Illustrates the proportion of total tracked time allocated to each tag, showing the relative contribution of each tag to the overall time tracked.
24
24
 
25
25
  Example:
@@ -35,13 +35,15 @@ Tracked time report [today]:
35
35
  | Total: | 02:00:00 | |
36
36
  +-------+------------+--------+----------+----------+----------+--------------------------+
37
37
 
38
- ⏳ ↦ 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
39
- ▂▂ ▇▇ ▅▅ ▄▄
38
+ ⏳ ↦ [ 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ]
39
+ [ ▂▂ ▇▇ ▅▅ ▄▄ ]
40
40
 
41
41
  Tag8: 50.0% ▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅
42
42
  Tag3: 50.0% ▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅▅
43
43
  ```
44
44
 
45
+ ![Timet monthly report](monthly_report.webp)
46
+
45
47
  ## Requirements
46
48
 
47
49
  - Ruby version: >= 3.0.0
@@ -233,7 +235,7 @@ Many people have contacted me asking how to contribute. Any contribution, from a
233
235
 
234
236
  **Bitcoin Address:**
235
237
  ```sh
236
- bc1qkg9me2jsuhpzu2hp9kkpxagwtf9ewnyfl4kszl
238
+ bc1qkg9me2jsuhpzu2hp9kkpxagwtf9ewnyfl4kszl
237
239
  ```
238
240
 
239
241
  ![Buy me a coffee!](btc.png)
@@ -45,10 +45,13 @@ module Timet
45
45
  # trigger a sound and notification after the specified time has elapsed.
46
46
  #
47
47
  # @param tag [String] The tag associated with the tracking session. This is a required parameter.
48
- # @param notes [String, nil] Optional notes to be associated with the tracking session. If not provided, it defaults to the value in `options[:notes]`.
49
- # @param pomodoro [Numeric, nil] Optional Pomodoro time in minutes. If not provided, it defaults to the value in `options[:pomodoro]`.
48
+ # @param notes [String, nil] Optional notes to be associated with the tracking session. If not provided, it
49
+ # defaults to the value in `options[:notes]`.
50
+ # @param pomodoro [Numeric, nil] Optional Pomodoro time in minutes. If not provided, it defaults to the value in
51
+ # `options[:pomodoro]`.
50
52
  #
51
- # @return [void] This method does not return a value; it performs side effects such as inserting a tracking item, playing a sound, sending a notification, and generating a summary.
53
+ # @return [void] This method does not return a value; it performs side effects such as inserting a tracking item,
54
+ # playing a sound, sending a notification, and generating a summary.
52
55
  #
53
56
  # @example Start a tracking session with a tag and notes
54
57
  # start('work', 'Starting work on project X', 25)
@@ -74,14 +77,17 @@ module Timet
74
77
  desc 'stop', 'stop time tracking'
75
78
  # Stops the current tracking session if there is one in progress.
76
79
  #
77
- # @return [void] This method does not return a value; it performs side effects such as updating the tracking item and generating a summary.
80
+ # @return [void] This method does not return a value; it performs side effects such as updating the tracking item
81
+ # and generating a summary.
78
82
  #
79
83
  # @example Stop the current tracking session
80
84
  # stop
81
85
  #
82
86
  # @note The method checks if the last tracking item is in progress by calling `@db.last_item_status`.
83
- # @note If the last item is in progress, it fetches the last item's ID using `@db.fetch_last_id` and updates it with the current timestamp.
84
- # @note The method then fetches the last item using `@db.last_item` and generates a summary if the result is not nil.
87
+ # @note If the last item is in progress, it fetches the last item's ID using `@db.fetch_last_id` and updates it
88
+ # with the current timestamp.
89
+ # @note The method then fetches the last item using `@db.last_item` and generates a summary if the result
90
+ # is not nil.
85
91
  def stop(display = nil)
86
92
  return unless @db.last_item_status == :in_progress
87
93
 
@@ -94,14 +100,16 @@ module Timet
94
100
  desc 'resume (r)', 'resume last task'
95
101
  # Resumes the last tracking session if it was completed.
96
102
  #
97
- # @return [void] This method does not return a value; it performs side effects such as resuming a tracking session or providing feedback.
103
+ # @return [void] This method does not return a value; it performs side effects such as resuming a tracking session
104
+ # or providing feedback.
98
105
  #
99
106
  # @example Resume the last tracking session
100
107
  # resume
101
108
  #
102
109
  # @note The method checks the status of the last tracking item using `@db.last_item_status`.
103
110
  # @note If the last item is in progress, it prints a message indicating that a task is currently being tracked.
104
- # @note If the last item is complete, it fetches the last item using `@db.last_item`, retrieves the tag and notes, and calls the `start` method to resume the tracking session.
111
+ # @note If the last item is complete, it fetches the last item using `@db.last_item`, retrieves the tag and notes,
112
+ # and calls the `start` method to resume the tracking session.
105
113
  def resume
106
114
  status = @db.last_item_status
107
115
 
@@ -121,12 +129,15 @@ module Timet
121
129
  desc 'summary (su) [filter] [tag] --csv=csv_filename',
122
130
  ' [filter] => [today (t), yesterday (y), week (w), month (m), [start_date]..[end_date]] [tag]'
123
131
  option :csv, type: :string, desc: 'Export to CSV file'
124
- # Generates a summary of tracking items based on the provided filter and tag, and optionally exports the summary to a CSV file.
132
+ # Generates a summary of tracking items based on the provided filter and tag, and optionally exports the summary
133
+ # to a CSV file.
125
134
  #
126
- # @param filter [String, nil] The filter to apply when generating the summary. Possible values include 'today', 'yesterday', 'week', 'month', or a date range in the format '[start_date]..[end_date]'.
135
+ # @param filter [String, nil] The filter to apply when generating the summary. Possible values include 'today',
136
+ # 'yesterday', 'week', 'month', or a date range in the format '[start_date]..[end_date]'.
127
137
  # @param tag [String, nil] The tag to filter the tracking items by.
128
138
  #
129
- # @return [void] This method does not return a value; it performs side effects such as displaying the summary and exporting to CSV if specified.
139
+ # @return [void] This method does not return a value; it performs side effects such as displaying the summary and
140
+ # exporting to CSV if specified.
130
141
  #
131
142
  # @example Generate a summary for today
132
143
  # summary('today')
@@ -139,7 +150,8 @@ module Timet
139
150
  #
140
151
  # @note The method initializes a `TimeReport` object with the database, filter, tag, and optional CSV filename.
141
152
  # @note The method calls `display` on the `TimeReport` object to show the summary.
142
- # @note If a CSV filename is provided and there are items to export, the method calls `export_sheet` to export the summary to a CSV file.
153
+ # @note If a CSV filename is provided and there are items to export, the method calls `export_sheet` to export the
154
+ # summary to a CSV file.
143
155
  # @note If no items are found to export, it prints a message indicating that no items were found.
144
156
  def summary(filter = nil, tag = nil)
145
157
  csv_filename = options[:csv]&.split('.')&.first
@@ -156,13 +168,17 @@ module Timet
156
168
 
157
169
  desc 'edit (e) [id] [field] [value]',
158
170
  'edit a task, [field] (notes, tag, start or end) and [value] are optional parameters'
159
- # Edits a specific tracking item by its ID, allowing the user to modify fields such as notes, tag, start time, or end time.
171
+ # Edits a specific tracking item by its ID, allowing the user to modify fields such as notes, tag, start time, or
172
+ # end time.
160
173
  #
161
174
  # @param id [Integer] The ID of the tracking item to be edited.
162
- # @param field [String, nil] The field to be edited. Possible values include 'notes', 'tag', 'start', or 'end'. If not provided, the user will be prompted to select a field.
163
- # @param new_value [String, nil] The new value to be set for the specified field. If not provided, the user will be prompted to enter a new value.
175
+ # @param field [String, nil] The field to be edited. Possible values include 'notes', 'tag', 'start', or 'end'.
176
+ # If not provided, the user will be prompted to select a field.
177
+ # @param new_value [String, nil] The new value to be set for the specified field. If not provided, the user will be
178
+ # prompted to enter a new value.
164
179
  #
165
- # @return [void] This method does not return a value; it performs side effects such as updating the tracking item and displaying the updated item.
180
+ # @return [void] This method does not return a value; it performs side effects such as updating the tracking item
181
+ # and displaying the updated item.
166
182
  #
167
183
  # @example Edit the notes of a tracking item with ID 1
168
184
  # edit(1, 'notes', 'Updated notes')
@@ -172,7 +188,8 @@ module Timet
172
188
  #
173
189
  # @note The method first attempts to find the tracking item by its ID using `@db.find_item(id)`.
174
190
  # @note If the item is found, it displays the current item details using `display_item(item)`.
175
- # @note If the field or new value is not provided, the user is prompted to select a field to edit and enter a new value.
191
+ # @note If the field or new value is not provided, the user is prompted to select a field to edit and enter
192
+ # a new value.
176
193
  # @note The method then validates and updates the item using `validate_and_update(item, field, new_value)`.
177
194
  # @note Finally, it displays the updated item details using `display_item(updated_item)`.
178
195
  def edit(id, field = nil, new_value = nil)
@@ -194,15 +211,18 @@ module Timet
194
211
  #
195
212
  # @param id [Integer] The ID of the tracking item to be deleted.
196
213
  #
197
- # @return [void] This method does not return a value; it performs side effects such as deleting the tracking item and displaying a confirmation message.
214
+ # @return [void] This method does not return a value; it performs side effects such as deleting the tracking item
215
+ # and displaying a confirmation message.
198
216
  #
199
217
  # @example Delete a tracking item with ID 1
200
218
  # delete(1)
201
219
  #
202
220
  # @note The method first attempts to find the tracking item by its ID using `@db.find_item(id)`.
203
221
  # @note If the item is found, it displays the item details using `TimeReport.new(@db).show_row(item)`.
204
- # @note The method then prompts the user for confirmation using `TTY::Prompt.new.yes?('Are you sure you want to delete this entry?')`.
205
- # @note If the user confirms, the method deletes the item and prints a confirmation message using `delete_item_and_print_message(id, "Deleted #{id}")`.
222
+ # @note The method then prompts the user for confirmation using `TTY::Prompt.new.yes?('Are you sure you want
223
+ # to delete this entry?')`.
224
+ # @note If the user confirms, the method deletes the item and prints a confirmation message using
225
+ # `delete_item_and_print_message(id, "Deleted #{id}")`.
206
226
  def delete(id)
207
227
  item = @db.find_item(id)
208
228
  return puts "No tracked time found for id: #{id}" unless item
@@ -216,14 +236,16 @@ module Timet
216
236
  desc 'cancel (c)', 'cancel active time tracking'
217
237
  # Cancels the active time tracking session by deleting the last tracking item.
218
238
  #
219
- # @return [void] This method does not return a value; it performs side effects such as deleting the active tracking item and displaying a confirmation message.
239
+ # @return [void] This method does not return a value; it performs side effects such as deleting the active tracking
240
+ # item and displaying a confirmation message.
220
241
  #
221
242
  # @example Cancel the active time tracking session
222
243
  # cancel
223
244
  #
224
245
  # @note The method fetches the ID of the last tracking item using `@db.fetch_last_id`.
225
246
  # @note It checks if the last item is in progress by comparing `@db.last_item_status` with `:complete`.
226
- # @note If the last item is in progress, it deletes the item and prints a confirmation message using `delete_item_and_print_message(id, "Canceled active time tracking #{id}")`.
247
+ # @note If the last item is in progress, it deletes the item and prints a confirmation message using
248
+ # `delete_item_and_print_message(id, "Canceled active time tracking #{id}")`.
227
249
  # @note If there is no active time tracking, it prints a message indicating that there is no active time tracking.
228
250
  def cancel
229
251
  id = @db.fetch_last_id
@@ -240,7 +262,8 @@ module Timet
240
262
  # MyClass.exit_on_failure? # => true
241
263
  #
242
264
  # @note This method is typically used in command-line applications to control the behavior when a command fails.
243
- # @note Returning `true` means that the application will exit immediately if a command fails, which is useful for ensuring that errors are handled gracefully.
265
+ # @note Returning `true` means that the application will exit immediately if a command fails, which is useful for
266
+ # ensuring that errors are handled gracefully.
244
267
  def self.exit_on_failure?
245
268
  true
246
269
  end
@@ -264,7 +287,8 @@ module Timet
264
287
  # @param id [Integer] The ID of the tracking item to be deleted.
265
288
  # @param message [String] The message to be printed after the item is deleted.
266
289
  #
267
- # @return [void] This method does not return a value; it performs side effects such as deleting the tracking item and printing a message.
290
+ # @return [void] This method does not return a value; it performs side effects such as deleting the tracking item
291
+ # and printing a message.
268
292
  #
269
293
  # @example Delete a tracking item with ID 1 and print a confirmation message
270
294
  # delete_item_and_print_message(1, 'Deleted item 1')
@@ -12,7 +12,8 @@ module Timet
12
12
  # @example Display the details of a tracking item
13
13
  # display_item(item)
14
14
  #
15
- # @note The method initializes a `TimeReport` object with the database and calls `show_row` to display the item details.
15
+ # @note The method initializes a `TimeReport` object with the database and calls `show_row` to display the
16
+ # item details.
16
17
  def display_item(item)
17
18
  TimeReport.new(@db).show_row(item)
18
19
  end
@@ -28,7 +29,8 @@ module Timet
28
29
  # prompt_for_new_value(item, 'notes')
29
30
  #
30
31
  # @note The method retrieves the current value of the field using `field_value`.
31
- # @note The method uses `TTY::Prompt.new` to prompt the user for a new value, displaying the current value in the prompt.
32
+ # @note The method uses `TTY::Prompt.new` to prompt the user for a new value, displaying the current value
33
+ # in the prompt.
32
34
  def prompt_for_new_value(item, field)
33
35
  current_value = field_value(item, field)
34
36
  prompt = TTY::Prompt.new(active_color: :green)
@@ -54,13 +56,15 @@ module Timet
54
56
  # @param item [Hash] The tracking item.
55
57
  # @param field [String] The field to retrieve the value for.
56
58
  #
57
- # @return [String, Time] The value of the specified field. If the field is 'start' or 'end', it returns the value as a Time object.
59
+ # @return [String, Time] The value of the specified field. If the field is 'start' or 'end', it returns the value
60
+ # as a Time object.
58
61
  #
59
62
  # @example Retrieve the value of the 'notes' field
60
63
  # field_value(item, 'notes')
61
64
  #
62
65
  # @note The method retrieves the index of the field from `Timet::Application::FIELD_INDEX`.
63
- # @note If the field is 'start' or 'end', the method converts the value to a Time object using `TimeHelper.timestamp_to_time`.
66
+ # @note If the field is 'start' or 'end', the method converts the value to a Time object
67
+ # using `TimeHelper.timestamp_to_time`.
64
68
  def field_value(item, field)
65
69
  index = Timet::Application::FIELD_INDEX[field]
66
70
  value = item[index]
@@ -89,15 +93,38 @@ module Timet
89
93
  #
90
94
  # @return [void]
91
95
  def play_sound_and_notify(time, tag)
92
- if RUBY_PLATFORM.downcase.include?('linux')
93
- pid = spawn("sleep #{time} && tput bel && /home/frank/Software/frankvielma/gems/timet/bin/timet stop 0 && notify-send --icon=clock 'Pomodoro session complete! (tag: #{tag}) Time for a break.' &")
94
- Process.wait(pid)
95
- elsif RUBY_PLATFORM.downcase.include?('darwin')
96
- pid = spawn("(sleep #{time} && afplay /System/Library/Sounds/Basso.aiff && osascript -e 'display notification \"Pomodoro session complete! Time for a break.\"') &")
97
- Process.wait(pid)
96
+ platform = RUBY_PLATFORM.downcase
97
+ if platform.include?('linux')
98
+ run_linux_session(time, tag)
99
+ elsif platform.include?('darwin')
100
+ run_mac_session(time, tag)
98
101
  else
99
102
  puts 'Unsupported operating system'
100
103
  end
101
104
  end
105
+
106
+ # Runs a Pomodoro session on a Linux system.
107
+ #
108
+ # @param time [Integer] The duration of the Pomodoro session in seconds.
109
+ # @param tag [String] A tag or label for the session, used in the notification message.
110
+ # @return [void]
111
+ def run_linux_session(time, tag)
112
+ notification_command = "notify-send --icon=clock 'Pomodoro session complete! (tag: #{tag}) Time for a break.'"
113
+ command = "sleep #{time} && tput bel && tt stop 0 && #{notification_command} &"
114
+ pid = spawn(command)
115
+ Process.wait(pid)
116
+ end
117
+
118
+ # Runs a Pomodoro session on a macOS system.
119
+ #
120
+ # @param time [Integer] The duration of the Pomodoro session in seconds.
121
+ # @param _tag [String] A tag or label for the session, not used in the notification message on macOS.
122
+ # @return [void]
123
+ def run_mac_session(time, _tag)
124
+ notification_command = "osascript -e 'display notification \"Pomodoro session complete! Time for a break.\"'"
125
+ command = "sleep #{time} && afplay /System/Library/Sounds/Basso.aiff && tt stop 0 && #{notification_command} &"
126
+ pid = spawn(command)
127
+ Process.wait(pid)
128
+ end
102
129
  end
103
130
  end
@@ -14,7 +14,8 @@ module Timet
14
14
  #
15
15
  # @param database_path [String] The path to the SQLite database file. Defaults to DEFAULT_DATABASE_PATH.
16
16
  #
17
- # @return [void] This method does not return a value; it performs side effects such as initializing the database connection and creating the necessary tables.
17
+ # @return [void] This method does not return a value; it performs side effects such as initializing the database
18
+ # connection and creating the necessary tables.
18
19
  #
19
20
  # @example Initialize a new Database instance with the default path
20
21
  # Database.new
@@ -22,7 +23,8 @@ module Timet
22
23
  # @example Initialize a new Database instance with a custom path
23
24
  # Database.new('/path/to/custom.db')
24
25
  #
25
- # @note The method creates a new SQLite3 database connection and initializes the necessary tables if they do not already exist.
26
+ # @note The method creates a new SQLite3 database connection and initializes the necessary tables if they
27
+ # do not already exist.
26
28
  def initialize(database_path = DEFAULT_DATABASE_PATH)
27
29
  @db = SQLite3::Database.new(database_path)
28
30
  create_table
@@ -31,7 +33,8 @@ module Timet
31
33
 
32
34
  # Creates the items table if it doesn't already exist.
33
35
  #
34
- # @return [void] This method does not return a value; it performs side effects such as executing SQL to create the table.
36
+ # @return [void] This method does not return a value; it performs side effects such as executing SQL to
37
+ # create the table.
35
38
  #
36
39
  # @example Create the items table
37
40
  # create_table
@@ -50,7 +53,8 @@ module Timet
50
53
 
51
54
  # Adds a new column named "notes" to the "items" table if it doesn't exist.
52
55
  #
53
- # @return [void] This method does not return a value; it performs side effects such as executing SQL to add the column.
56
+ # @return [void] This method does not return a value; it performs side effects such as executing SQL to add
57
+ # the column.
54
58
  #
55
59
  # @example Add the notes column to the items table
56
60
  # add_notes
@@ -73,7 +77,8 @@ module Timet
73
77
  # @param tag [String] The tag associated with the item.
74
78
  # @param notes [String] The notes associated with the item.
75
79
  #
76
- # @return [void] This method does not return a value; it performs side effects such as executing SQL to insert the item.
80
+ # @return [void] This method does not return a value; it performs side effects such as executing SQL
81
+ # to insert the item.
77
82
  #
78
83
  # @example Insert a new item into the items table
79
84
  # insert_item(1633072800, 'work', 'Completed task X')
@@ -89,7 +94,8 @@ module Timet
89
94
  # @param field [String] The field to be updated.
90
95
  # @param value [String, Integer, nil] The new value for the specified field.
91
96
  #
92
- # @return [void] This method does not return a value; it performs side effects such as executing SQL to update the item.
97
+ # @return [void] This method does not return a value; it performs side effects such as executing SQL
98
+ # to update the item.
93
99
  #
94
100
  # @example Update the tag of an item with ID 1
95
101
  # update_item(1, 'tag', 'updated_work')
@@ -105,7 +111,8 @@ module Timet
105
111
  #
106
112
  # @param id [Integer] The ID of the item to be deleted.
107
113
  #
108
- # @return [void] This method does not return a value; it performs side effects such as executing SQL to delete the item.
114
+ # @return [void] This method does not return a value; it performs side effects such as executing SQL
115
+ # to delete the item.
109
116
  #
110
117
  # @example Delete an item with ID 1
111
118
  # delete_item(1)
@@ -174,7 +181,8 @@ module Timet
174
181
  # @example Fetch all items from today
175
182
  # all_items
176
183
  #
177
- # @note The method executes SQL to fetch all items from the 'items' table that have a start time greater than or equal to today.
184
+ # @note The method executes SQL to fetch all items from the 'items' table that have a start time greater than
185
+ # or equal to today.
178
186
  def all_items
179
187
  execute_sql("SELECT * FROM items where start >= '#{Date.today.to_time.to_i}' ORDER BY id DESC")
180
188
  end
@@ -199,7 +207,8 @@ module Timet
199
207
 
200
208
  # Closes the database connection.
201
209
  #
202
- # @return [void] This method does not return a value; it performs side effects such as closing the database connection.
210
+ # @return [void] This method does not return a value; it performs side effects such as closing the
211
+ # database connection.
203
212
  #
204
213
  # @example Close the database connection
205
214
  # close
@@ -218,7 +227,8 @@ module Timet
218
227
  # @example Convert 3661 seconds to HH:MM:SS format
219
228
  # seconds_to_hms(3661) # => '01:01:01'
220
229
  #
221
- # @note The method converts the given number of seconds into hours, minutes, and seconds, and formats them as HH:MM:SS.
230
+ # @note The method converts the given number of seconds into hours, minutes, and seconds, and formats
231
+ # them as HH:MM:SS.
222
232
  def seconds_to_hms(seconds)
223
233
  hours, remainder = seconds.divmod(3600)
224
234
  minutes, seconds = remainder.divmod(60)
@@ -4,9 +4,22 @@ module Timet
4
4
  # This module is responsible for formatting the output of the `timet` application.
5
5
  # It provides methods for formatting the table header, separators, and rows.
6
6
  module Formatter
7
+ CHAR_MAPPING = {
8
+ 0..120 => '_',
9
+ 121..450 => '▁',
10
+ 451..900 => '▂',
11
+ 901..1350 => '▃',
12
+ 1351..1800 => '▄',
13
+ 1801..2250 => '▅',
14
+ 2251..2700 => '▆',
15
+ 2701..3150 => '▇',
16
+ 3151..3600 => '█'
17
+ }.freeze
18
+
7
19
  # Formats the header of the time tracking report table.
8
20
  #
9
- # @return [void] This method does not return a value; it performs side effects such as printing the formatted header.
21
+ # @return [void] This method does not return a value; it performs side effects such as printing
22
+ # the formatted header.
10
23
  #
11
24
  # @example Format and print the table header
12
25
  # format_table_header
@@ -16,7 +29,7 @@ module Timet
16
29
  header = <<~TABLE
17
30
  Tracked time report \e[5m\u001b[31m[#{@filter}]\033[0m:
18
31
  #{format_table_separator}
19
- \033[32m| Id | Date | Tag | Start | End | Duration | Notes |\033[0m
32
+ \033[32m| Id | Date | Tag | Start | End | Duration | Notes |\033[0m
20
33
  #{format_table_separator}
21
34
  TABLE
22
35
  puts header
@@ -27,11 +40,11 @@ module Timet
27
40
  # @return [String] The formatted separator line.
28
41
  #
29
42
  # @example Get the formatted table separator
30
- # format_table_separator # => '+-------+------------+--------+----------+----------+----------+--------------------------+'
43
+ # format_table_separator # => '+-------+------------+--------+----------+----------+----------+------------+'
31
44
  #
32
45
  # @note The method returns a string representing the separator line for the table.
33
46
  def format_table_separator
34
- '+-------+------------+--------+----------+----------+----------+--------------------------+'
47
+ '+-------+------------+--------+----------+----------+----------+--------------------+'
35
48
  end
36
49
 
37
50
  # Formats a row of the time tracking report table.
@@ -59,10 +72,12 @@ module Timet
59
72
  #
60
73
  # @note The method truncates the notes to a maximum of 20 characters and pads them to a fixed width.
61
74
  def format_notes(notes)
62
- return ' ' * 23 if notes.nil?
75
+ spaces = 17
76
+ return ' ' * spaces unless notes
63
77
 
64
- notes = "#{notes.slice(0, 20)}..." if notes.length > 20
65
- notes.ljust(23)
78
+ max_length = spaces - 3
79
+ notes = "#{notes.slice(0, max_length)}..." if notes.length > max_length
80
+ notes.ljust(spaces)
66
81
  end
67
82
 
68
83
  # @!method format_tag_distribution(duration_by_tag)
@@ -77,59 +92,95 @@ module Timet
77
92
  #
78
93
  # @param duration_by_tag [Hash<String, Integer>] A hash where keys are tags and values are durations in seconds.
79
94
  # @return [void] This method outputs the formatted tag distribution to the console.
80
- def format_tag_distribution(duration_by_tag)
81
- block = '▅'
95
+ def format_tag_distribution(duration_by_tag, colors)
82
96
  total = duration_by_tag.values.sum
83
97
  return unless total.positive?
84
98
 
85
99
  factor = duration_by_tag.size < 3 ? 2 : 1
86
100
  sorted_duration_by_tag = duration_by_tag.sort_by { |_, duration| -duration }
101
+ process_and_print_tags(sorted_duration_by_tag, factor, total, colors)
102
+ end
87
103
 
104
+ # Processes and prints the tag distribution information.
105
+ #
106
+ # @param sorted_duration_by_tag [Array<Array(String, Numeric)>] An array of arrays where each inner array contains a
107
+ # tag and its corresponding duration, sorted by duration in descending order.
108
+ # @param factor [Numeric] The factor used to adjust the bar length.
109
+ # @param total [Numeric] The total duration of all tags combined.
110
+ # @return [void] This method outputs the tag distribution information to the standard output.
111
+ def process_and_print_tags(*args)
112
+ sorted_duration_by_tag, factor, total, colors = args
113
+ block = '▅'
88
114
  sorted_duration_by_tag.each do |tag, duration|
89
- value = (duration.to_f / total * 100).round(2)
90
- puts "#{tag.rjust(8)}: #{value.to_s.rjust(7)}% \u001b[38;5;#{rand(256)}m#{block * (value / factor).to_i}\u001b[0m"
115
+ value, bar_length = calculate_value_and_bar_length(duration, total, factor)
116
+ puts "#{tag.rjust(8)}: #{value.to_s.rjust(7)}% \u001b[38;5;#{colors[tag] + 1}m#{block * bar_length}\u001b[0m"
91
117
  end
92
118
  end
93
119
 
94
- # Prints the entire time block chart.
120
+ # Calculates the value and bar length for a given duration, total duration, and factor.
95
121
  #
96
- # This method orchestrates the printing of the entire time block chart by calling
97
- # the `print_header` and `print_blocks` methods. It also prints the separator line
98
- # between the header and the blocks, and adds a double newline at the end for
99
- # separation.
122
+ # @param duration [Numeric] The duration for the current tag.
123
+ # @param total [Numeric] The total duration.
124
+ # @param factor [Numeric] A factor to adjust the formatting.
125
+ # @return [Array<(Float, Integer)>] An array containing the calculated value and bar length.
100
126
  #
101
- # @param time_block [Hash] A hash where the keys are formatted hour strings
102
- # (e.g., "00", "01") and the values are the corresponding
103
- # values to determine the block character.
104
127
  # @example
105
- # time_block = { "00" => 100, "01" => 200, ..., "23" => 300 }
106
- # print_time_block_chart(time_block)
107
- # # Output:
108
- # # 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
109
- # # ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █
110
- # #
111
- # # (followed by two newlines)
128
+ # calculate_value_and_bar_length(50, 100, 2) #=> [50.0, 25]
129
+ def calculate_value_and_bar_length(duration, total, factor)
130
+ value = (duration.to_f / total * 100).round(2)
131
+ bar_length = (value / factor).round
132
+ [value, bar_length]
133
+ end
134
+
135
+ # Prints a time block chart based on the provided time block and colors.
136
+ #
137
+ # @param time_block [Hash] A hash where the keys are time blocks and the values are hashes of time slots and their
138
+ # corresponding values.
139
+ # Example: { "block1" => { 10 => "value1", 11 => "value2" }, "block2" => { 12 => "value3" } }
140
+ # @param colors [Hash] A hash where the keys are time slots and the values are the colors to be used
141
+ # for those slots.
142
+ # Example: { 10 => "red", 11 => "blue", 12 => "green" }
112
143
  #
113
- def print_time_block_chart(time_block)
114
- print_header
115
- print ' ┗ '
116
- print_blocks(time_block)
144
+ # @return [void] This method does not return a value; it prints the chart directly to the output.
145
+ #
146
+ # @example
147
+ # time_block = { "block1" => { 10 => "value1", 11 => "value2" }, "block2" => { 12 => "value3" } }
148
+ # colors = { 10 => "red", 11 => "blue", 12 => "green" }
149
+ # print_time_block_chart(time_block, colors)
150
+ #
151
+ # @note This method relies on two helper methods: `print_header` and `print_blocks`.
152
+ # Ensure these methods are defined and available in the scope where `print_time_block_chart` is called.
153
+ #
154
+ # @see #print_header
155
+ # @see #print_blocks
156
+ def print_time_block_chart(time_block, colors)
157
+ start_time = time_block.values.map(&:keys).flatten.uniq.min.to_i
158
+ print_header(start_time)
159
+ print_blocks(time_block, colors, start_time)
117
160
  end
118
161
 
119
162
  # Prints the header of the time block chart.
120
163
  #
121
- # This method outputs the header line of the chart, which includes the hours
122
- # from 00 to 23, formatted and aligned for readability.
164
+ # The header includes a visual representation of the time slots from the given start time to 23.
165
+ # Each time slot is formatted as a two-digit number and aligned to the right within a fixed width.
166
+ #
167
+ # @param start_time [Integer] The starting time for the chart. This should be an integer between 0 and 23.
168
+ #
169
+ # @return [void] This method does not return a value; it prints the header directly to the output.
123
170
  #
124
171
  # @example
125
- # print_header
172
+ # print_header(10)
126
173
  # # Output:
127
- # # ⏳ ↦ ┏ 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
174
+ # #
175
+ # # ⏳ ↦ [ 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
128
176
  #
129
- def print_header
177
+ # @note The method assumes that the start_time is within the valid range of 0 to 23.
178
+ # If the start_time is outside this range, the output may not be as expected.
179
+ def print_header(start_time)
130
180
  puts
131
- print '⏳ ↦ '
132
- (0..23).each { |hour| print format('%02d', hour).ljust(4) }
181
+ print ' ⏳ ↦ [ '
182
+ (start_time..23).each { |hour| print format('%02d', hour).ljust(4) }
183
+ print ']'
133
184
  puts
134
185
  end
135
186
 
@@ -150,14 +201,72 @@ module Timet
150
201
  # #
151
202
  # # (followed by two newlines)
152
203
  #
153
- def print_blocks(time_block)
204
+ def print_blocks(time_block, colors, start_time)
154
205
  return unless time_block
155
206
 
156
- (0..23).each do |hour|
157
- block_char = get_block_char(time_block[format('%02d', hour)])
158
- print (block_char * 2).ljust(4)
207
+ time_block.each_key do |item|
208
+ print "#{item} "
209
+ time_block_initial = time_block[item]
210
+ print_time_blocks(start_time, time_block_initial, colors)
211
+ puts
212
+ end
213
+ puts "\n"
214
+ end
215
+
216
+ # Prints time blocks for each hour from the start time to 23.
217
+ #
218
+ # @param start_time [Integer] The starting hour for printing time blocks.
219
+ # @param time_block_initial [Hash] A hash containing time block data, where keys are formatted hours and values
220
+ # are arrays containing block data.
221
+ # @param colors [Hash] A hash mapping tags to color codes.
222
+ # @return [void]
223
+ #
224
+ # @example
225
+ # time_block_initial = {
226
+ # '01' => ['block_char_data', 'tag']
227
+ # }
228
+ # colors = { 'tag' => 1 }
229
+ # print_time_blocks(1, time_block_initial, colors) # Prints time blocks for hours 1 to 23
230
+ def print_time_blocks(start_time, time_block_initial, colors)
231
+ (start_time..23).each do |hour|
232
+ tag, block_char = get_formatted_block_char(hour, time_block_initial)
233
+ print_colored_block(block_char, tag, colors)
159
234
  end
160
- puts "\n\n"
235
+ end
236
+
237
+ # Returns the formatted block character and its associated tag for a given hour.
238
+ #
239
+ # @param hour [Integer] The hour for which to retrieve the block character.
240
+ # @param time_block_initial [Hash] A hash containing time block data, where keys are formatted hours and values
241
+ # are arrays containing block data.
242
+ # @return [Array<(String, String)>] An array containing the tag and the block character.
243
+ #
244
+ # @example
245
+ # time_block_initial = {
246
+ # '01' => ['block_char_data', 'tag']
247
+ # }
248
+ # get_formatted_block_char(1, time_block_initial) #=> ['tag', 'block_char']
249
+ def get_formatted_block_char(hour, time_block_initial)
250
+ formatted_hour = format('%02d', hour)
251
+ hour_data = time_block_initial[formatted_hour]
252
+ tag = hour_data&.last
253
+ [tag, get_block_char(hour_data&.first)]
254
+ end
255
+
256
+ # Prints a colored block character based on the provided tag and block character.
257
+ #
258
+ # @param block_char [String] The block character to be printed.
259
+ # @param tag [String] The tag associated with the block character, used to determine the color.
260
+ # @param colors [Hash] A hash mapping tags to color codes.
261
+ #
262
+ # @example
263
+ # colors = { 'tag' => 1 }
264
+ # print_colored_block('X', 'tag', colors) # Prints a colored block character 'XX'
265
+ def print_colored_block(block_char, tag, colors)
266
+ color_code = colors[tag]
267
+ block = block_char * 2
268
+ colored_block = color_code ? "\u001b[38;5;#{color_code + 1}m#{block}\u001b[0m " : block
269
+ print colored_block.ljust(4)
161
270
  end
162
271
 
163
272
  # Determines the block character based on the value.
@@ -165,19 +274,9 @@ module Timet
165
274
  # @param value [Integer] The value to determine the block character for.
166
275
  # @return [String] The block character corresponding to the value.
167
276
  def get_block_char(value)
168
- range_to_char = {
169
- 0..120 => ' ',
170
- 121..450 => '',
171
- 451..900 => '▂',
172
- 901..1350 => '▃',
173
- 1351..1800 => '▄',
174
- 1801..2250 => '▅',
175
- 2251..2700 => '▆',
176
- 2701..3150 => '▇',
177
- 3151..3600 => '█'
178
- }
179
-
180
- range_to_char.find { |range, _| range.include?(value) }&.last || ' '
277
+ return ' ' unless value
278
+
279
+ CHAR_MAPPING.find { |range, _| range.include?(value) }&.last || ' '
181
280
  end
182
281
  end
183
282
  end
@@ -7,7 +7,8 @@ module Timet
7
7
  #
8
8
  # @param result [Array] The result set containing time tracking items.
9
9
  #
10
- # @return [Symbol] The status of the time tracking result. Possible values are :no_items, :in_progress, or :complete.
10
+ # @return [Symbol] The status of the time tracking result. Possible values are
11
+ # :no_items, :in_progress, or :complete.
11
12
  #
12
13
  # @example Determine the status of an empty result set
13
14
  # StatusHelper.determine_status([]) # => :no_items
@@ -15,7 +15,7 @@ module Timet
15
15
  # @example Format a timestamp
16
16
  # TimeHelper.format_time(1633072800) # => '2021-10-01 12:00:00'
17
17
  def self.format_time(timestamp)
18
- return nil if timestamp.nil?
18
+ return nil unless timestamp
19
19
 
20
20
  Time.at(timestamp).strftime('%Y-%m-%d %H:%M:%S')
21
21
  end
@@ -28,7 +28,7 @@ module Timet
28
28
  # @example Convert a timestamp to a date string
29
29
  # TimeHelper.timestamp_to_date(1633072800) # => '2021-10-01'
30
30
  def self.timestamp_to_date(timestamp)
31
- return nil if timestamp.nil?
31
+ return nil unless timestamp
32
32
 
33
33
  Time.at(timestamp).strftime('%Y-%m-%d')
34
34
  end
@@ -41,7 +41,7 @@ module Timet
41
41
  # @example Convert a timestamp to a time string
42
42
  # TimeHelper.timestamp_to_time(1633072800) # => '12:00:00'
43
43
  def self.timestamp_to_time(timestamp)
44
- return nil if timestamp.nil?
44
+ return nil unless timestamp
45
45
 
46
46
  Time.at(timestamp).strftime('%H:%M:%S')
47
47
  end
@@ -186,53 +186,51 @@ module Timet
186
186
  # result = count_seconds_per_hour_block(start_time, end_time)
187
187
  # # Output: {"08"=>1800, "09"=>1800, "10"=>3600, "11"=>1200}
188
188
  #
189
- def self.count_seconds_per_hour_block(start_time, end_time)
189
+ def self.count_seconds_per_hour_block(start_time, end_time, tag)
190
190
  hour_blocks = Hash.new(0)
191
191
 
192
192
  current_time = Time.at(start_time)
193
193
  end_time = Time.at(end_time || current_timestamp)
194
194
 
195
195
  while current_time < end_time
196
- current_hour = current_time.hour
197
- next_hour_boundary = Time.new(current_time.year, current_time.month, current_time.day, current_hour + 1)
198
-
199
- block_end_time = [next_hour_boundary, end_time].min
200
- seconds_in_block = (block_end_time - current_time).to_i
201
-
202
- hour_block = current_time.strftime('%H')
203
- hour_blocks[hour_block] += seconds_in_block
196
+ block_end_time, hour_blocks = calculate_block_end_time_and_seconds(current_time, end_time, hour_blocks)
204
197
 
205
198
  current_time = block_end_time
206
199
  end
207
200
 
208
- hour_blocks
201
+ append_tag_to_hour_blocks(hour_blocks, tag)
209
202
  end
210
203
 
211
- # Aggregates the values of the same keys from an array of hashes.
212
- #
213
- # This method takes an array of hashes, reverses it, and then aggregates the values
214
- # for the same keys into a single hash. If a key appears in multiple hashes, its
215
- # values are summed.
216
- #
217
- # @param time_block [Array<Hash>] An array of hashes where each hash contains key-value pairs.
218
- # @return [Hash] A hash where the keys are the aggregated keys from the input hashes
219
- # and the values are the summed values for each key.
220
- # @example
221
- # time_block = [
222
- # {"01": 10},
223
- # {"01": 30},
224
- # {"02": 50}
225
- # ]
226
- # result = aggregate_hash_values(time_block)
227
- # # Output: {"01"=>40, "02"=>50}
228
- #
229
- def self.aggregate_hash_values(time_block)
230
- time_block.reverse.each_with_object({}) do |hash, acc|
231
- hash.each do |key, value|
232
- acc[key] ||= 0
233
- acc[key] += value
234
- end
204
+ # Calculates the end time of the current block and the number of seconds in the block.
205
+ # Additionally, it updates the `hour_blocks` hash with the number of seconds for the current hour block.
206
+ #
207
+ # @param current_time [Time] The current time.
208
+ # @param end_time [Time] The end time of the overall period.
209
+ # @param hour_blocks [Hash] A hash where each key represents an hour block and the value is the number of seconds
210
+ # in that block.
211
+ # @return [Array<(Time, Hash)>] An array containing the end time of the current block and the updated
212
+ # `hour_blocks` hash.
213
+ def self.calculate_block_end_time_and_seconds(current_time, end_time, hour_blocks)
214
+ current_hour = current_time.hour
215
+ next_hour_boundary = Time.new(current_time.year, current_time.month, current_time.day, current_hour + 1)
216
+
217
+ block_end_time = [next_hour_boundary, end_time].min
218
+ seconds_in_block = (block_end_time - current_time).to_i
219
+ hour_block = current_time.strftime('%H')
220
+ hour_blocks[hour_block] += seconds_in_block
221
+
222
+ [block_end_time, hour_blocks]
223
+ end
224
+
225
+ # @param hour_blocks [Hash] A hash where each key represents an hour block and the value is some data associated
226
+ # with that hour block.
227
+ # @param tag [Object] The tag to append to each value in the hash.
228
+ # @return [Hash] The modified hash with the tag appended to each value.
229
+ def self.append_tag_to_hour_blocks(hour_blocks, tag)
230
+ hour_blocks.each do |key, value|
231
+ hour_blocks[key] = [value, tag]
235
232
  end
233
+ hour_blocks
236
234
  end
237
235
  end
238
236
  end
@@ -4,6 +4,7 @@ require 'date'
4
4
  require 'csv'
5
5
  require_relative 'status_helper'
6
6
  require_relative 'formatter'
7
+ require_relative 'time_report_helper'
7
8
 
8
9
  module Timet
9
10
  # The TimeReport class is responsible for displaying a report of tracked time
@@ -11,6 +12,7 @@ module Timet
11
12
  # a formatted table with the relevant information.
12
13
  class TimeReport
13
14
  include Formatter
15
+ include TimeReportHelper
14
16
 
15
17
  # Provides access to the database instance.
16
18
  attr_reader :db
@@ -24,11 +26,13 @@ module Timet
24
26
  # Initializes a new instance of the TimeReport class.
25
27
  #
26
28
  # @param db [Database] The database instance to use for fetching data.
27
- # @param filter [String, nil] The filter to apply when fetching items. Possible values include 'today', 'yesterday', 'week', 'month', or a date range in the format 'YYYY-MM-DD..YYYY-MM-DD'.
29
+ # @param filter [String, nil] The filter to apply when fetching items. Possible values include 'today',
30
+ # 'yesterday', 'week', 'month', or a date range in the format 'YYYY-MM-DD..YYYY-MM-DD'.
28
31
  # @param tag [String, nil] The tag to filter the items by.
29
32
  # @param csv [String, nil] The filename to use when exporting the report to CSV.
30
33
  #
31
- # @return [void] This method does not return a value; it performs side effects such as initializing the instance variables.
34
+ # @return [void] This method does not return a value; it performs side effects such as initializing the
35
+ # instance variables.
32
36
  #
33
37
  # @example Initialize a new TimeReport instance with a filter and tag
34
38
  # TimeReport.new(db, 'today', 'work', 'report.csv')
@@ -51,23 +55,14 @@ module Timet
51
55
  return puts 'No tracked time found for the specified filter.' if items.empty?
52
56
 
53
57
  format_table_header
54
- duration_by_tag = Hash.new(0)
55
- time_block = []
56
- items.each_with_index do |item, idx|
57
- date = TimeHelper.extract_date(items, idx)
58
- display_time_entry(item, date)
59
- time_block << TimeHelper.count_seconds_per_hour_block(item[1], item[2])
60
- duration_by_tag[item[3]] += TimeHelper.calculate_duration(item[1], item[2])
61
- end
58
+ time_block, duration_by_tag = process_time_entries
62
59
  puts format_table_separator
63
60
  total
64
61
 
65
- if Time.now.to_i - items.map { |x| x[1] }.min < 86_400
66
- time_block_reverse = TimeHelper.aggregate_hash_values(time_block)
67
- print_time_block_chart(time_block_reverse)
68
- end
62
+ colors = duration_by_tag.map { |x| x[0] }.sort.each_with_index.to_h
63
+ print_time_block_chart(time_block, colors)
69
64
 
70
- format_tag_distribution(duration_by_tag)
65
+ format_tag_distribution(duration_by_tag, colors)
71
66
  end
72
67
 
73
68
  # Displays a single row of the report.
@@ -117,33 +112,10 @@ module Timet
117
112
  def write_csv(file_name)
118
113
  CSV.open(file_name, 'w') do |csv|
119
114
  csv << %w[ID Start End Tag Notes]
120
- items.each do |item|
121
- csv << format_item(item)
122
- end
115
+ write_csv_rows(csv)
123
116
  end
124
117
  end
125
118
 
126
- # Formats an item for CSV export.
127
- #
128
- # @param item [Array] The item to format.
129
- #
130
- # @return [Array] The formatted item.
131
- #
132
- # @example Format an item for CSV export
133
- # format_item(item)
134
- #
135
- # @note The method formats the item's ID, start time, end time, tag, and notes.
136
- def format_item(item)
137
- id, start_time, end_time, tags, notes = item
138
- [
139
- id,
140
- TimeHelper.format_time(start_time),
141
- TimeHelper.format_time(end_time),
142
- tags,
143
- notes
144
- ]
145
- end
146
-
147
119
  # Displays a single time entry in the report.
148
120
  #
149
121
  # @param item [Array] The item to display.
@@ -178,7 +150,7 @@ module Timet
178
150
  total = @items.map do |item|
179
151
  TimeHelper.calculate_duration(item[1], item[2])
180
152
  end.sum
181
- puts "|#{' ' * 43}\033[94mTotal: | #{@db.seconds_to_hms(total).rjust(8)} |\033[0m |"
153
+ puts "|#{' ' * 43}\033[94mTotal: | #{@db.seconds_to_hms(total).rjust(8)} |\033[0m |"
182
154
  puts format_table_separator
183
155
  end
184
156
 
@@ -206,24 +178,6 @@ module Timet
206
178
  end
207
179
  end
208
180
 
209
- # Provides predefined date ranges for filtering.
210
- #
211
- # @return [Hash] A hash containing predefined date ranges.
212
- #
213
- # @example Get the predefined date ranges
214
- # date_ranges
215
- #
216
- # @note The method returns a hash with predefined date ranges for 'today', 'yesterday', 'week', and 'month'.
217
- def date_ranges
218
- today = Date.today
219
- {
220
- 'today' => [today, nil],
221
- 'yesterday' => [today - 1, nil],
222
- 'week' => [today - 7, today + 1],
223
- 'month' => [today - 30, today + 1]
224
- }
225
- end
226
-
227
181
  # Filters the items by date range and tag.
228
182
  #
229
183
  # @param start_date [Date] The start date of the range.
@@ -271,22 +225,5 @@ module Timet
271
225
 
272
226
  'today'
273
227
  end
274
-
275
- # Validates the date format.
276
- #
277
- # @param date_string [String] The date string to validate.
278
- #
279
- # @return [Boolean] True if the date format is valid, otherwise false.
280
- #
281
- # @example Validate the date format
282
- # valid_date_format?('2021-10-01') # => true
283
- #
284
- # @note The method validates the date format for single dates and date ranges.
285
- def valid_date_format?(date_string)
286
- date_format_single = /^\d{4}-\d{2}-\d{2}$/
287
- date_format_range = /^\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}$/
288
-
289
- date_string.match?(date_format_single) || date_string.match?(date_format_range)
290
- end
291
228
  end
292
229
  end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Timet
4
+ # The TimeReportHelper module provides a collection of utility methods for processing and formatting time report data.
5
+ # It includes methods for processing time entries, handling time blocks, formatting items for CSV export,
6
+ # and validating date formats.
7
+ # This module is designed to be included in classes that require time report processing functionalities.
8
+ module TimeReportHelper
9
+ # Processes each time entry in the items array and updates the time block and duration by tag.
10
+ #
11
+ # @return [Array<(Hash, Hash)>] An array containing the updated time block and duration by tag.
12
+ #
13
+ # @example
14
+ # items = [
15
+ # [start_time1, end_time1, tag1],
16
+ # [start_time2, end_time2, tag2]
17
+ # ]
18
+ # process_time_entries
19
+ # #=> [{ '2024-10-21' => { 8 => [duration1, tag1], 9 => [duration2, tag2] } }, { tag1 => total_duration1,
20
+ # tag2 => total_duration2 }]
21
+ def process_time_entries
22
+ duration_by_tag = Hash.new(0)
23
+ time_block = Hash.new { |hash, key| hash[key] = {} }
24
+
25
+ items.each_with_index do |item, idx|
26
+ display_time_entry(item, TimeHelper.extract_date(items, idx))
27
+ start_time = item[1]
28
+ end_time = item[2]
29
+ tag = item[3]
30
+ time_block = process_time_block_item(start_time, end_time, tag, time_block)
31
+
32
+ duration_by_tag[tag] += TimeHelper.calculate_duration(start_time, end_time)
33
+ end
34
+ [time_block, duration_by_tag]
35
+ end
36
+
37
+ # Processes a time block item and updates the time block hash.
38
+ #
39
+ # @param start_time [Time] The start time of the time block.
40
+ # @param end_time [Time] The end time of the time block.
41
+ # @param tag [String] The tag associated with the time block.
42
+ # @param time_block [Hash] A hash containing time block data, where keys are dates and values are hashes of time
43
+ # slots and their corresponding values.
44
+ # @return [Hash] The updated time block hash.
45
+ #
46
+ # @example
47
+ # start_time = Time.new(2024, 10, 21, 8, 0, 0)
48
+ # end_time = Time.new(2024, 10, 21, 9, 0, 0)
49
+ # tag = 'work'
50
+ # time_block = {}
51
+ # process_time_block_item(start_time, end_time, tag, time_block)
52
+ # #=> { '2024-10-21' => { 8 => [duration, 'work'] } }
53
+ def process_time_block_item(*args)
54
+ start_time, end_time, tag, time_block = args
55
+ block_hour = TimeHelper.count_seconds_per_hour_block(start_time, end_time, tag)
56
+ date_line = TimeHelper.timestamp_to_date(start_time)
57
+ time_block[date_line] = add_hashes(time_block[date_line], block_hour)
58
+ time_block
59
+ end
60
+
61
+ # Provides predefined date ranges for filtering.
62
+ #
63
+ # @return [Hash] A hash containing predefined date ranges.
64
+ #
65
+ # @example Get the predefined date ranges
66
+ # date_ranges
67
+ #
68
+ # @note The method returns a hash with predefined date ranges for 'today', 'yesterday', 'week', and 'month'.
69
+ def date_ranges
70
+ today = Date.today
71
+ tomorrow = today + 1
72
+ {
73
+ 'today' => [today, nil],
74
+ 'yesterday' => [today - 1, nil],
75
+ 'week' => [today - 7, tomorrow],
76
+ 'month' => [today - 30, tomorrow]
77
+ }
78
+ end
79
+
80
+ # Formats an item for CSV export.
81
+ #
82
+ # @param item [Array] The item to format.
83
+ #
84
+ # @return [Array] The formatted item.
85
+ #
86
+ # @example Format an item for CSV export
87
+ # format_item(item)
88
+ #
89
+ # @note The method formats the item's ID, start time, end time, tag, and notes.
90
+ def format_item(item)
91
+ id, start_time, end_time, tags, notes = item
92
+ [
93
+ id,
94
+ TimeHelper.format_time(start_time),
95
+ TimeHelper.format_time(end_time),
96
+ tags,
97
+ notes
98
+ ]
99
+ end
100
+
101
+ # Validates the date format.
102
+ #
103
+ # @param date_string [String] The date string to validate.
104
+ #
105
+ # @return [Boolean] True if the date format is valid, otherwise false.
106
+ #
107
+ # @example Validate the date format
108
+ # valid_date_format?('2021-10-01') # => true
109
+ #
110
+ # @note The method validates the date format for single dates and date ranges.
111
+ def valid_date_format?(date_string)
112
+ date_format_single = /^\d{4}-\d{2}-\d{2}$/
113
+ date_format_range = /^\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}$/
114
+
115
+ date_string.match?(date_format_single) || date_string.match?(date_format_range)
116
+ end
117
+
118
+ # Merges two hashes, summing the numeric values of corresponding keys.
119
+ #
120
+ # @param base_hash [Hash] The base hash to which the additional hash will be merged.
121
+ # @param additional_hash [Hash] The additional hash whose values will be added to the base hash.
122
+ # @return [Hash] A new hash with the summed values.
123
+ #
124
+ # @example
125
+ # base_hash = { 'key1' => [10, 'tag1'], 'key2' => [20, 'tag2'] }
126
+ # additional_hash = { 'key1' => [5, 'tag1'], 'key3' => [15, 'tag3'] }
127
+ # add_hashes(base_hash, additional_hash)
128
+ # #=> { 'key1' => [15, 'tag1'], 'key2' => [20, 'tag2'], 'key3' => [15, 'tag3'] }
129
+ def add_hashes(base_hash, additional_hash)
130
+ base_hash.merge(additional_hash) do |_key, old_value, new_value|
131
+ summed_number = old_value[0] + new_value[0]
132
+ [summed_number, old_value[1]]
133
+ end
134
+ end
135
+
136
+ # Writes the CSV rows for the time report.
137
+ #
138
+ # @param csv [CSV] The CSV object to which the rows will be written.
139
+ # @return [void]
140
+ #
141
+ # @example
142
+ # csv = CSV.new(file)
143
+ # write_csv_rows(csv)
144
+ def write_csv_rows(csv)
145
+ items.each do |item|
146
+ csv << format_item(item)
147
+ end
148
+ end
149
+ end
150
+ end
data/lib/timet/version.rb CHANGED
@@ -6,6 +6,6 @@ module Timet
6
6
  # @return [String] The version number in the format 'major.minor.patch'.
7
7
  #
8
8
  # @example Get the version of the Timet application
9
- # Timet::VERSION # => '1.2.0'
10
- VERSION = '1.2.0'
9
+ # Timet::VERSION # => '1.3.0'
10
+ VERSION = '1.3.0'
11
11
  end
data/lib/timet.rb CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  # Require the Timet::Application class from the 'timet/application' file.
4
4
  #
5
- # @note This statement loads the Timet::Application class, which is responsible for handling the command-line interface and user commands.
5
+ # @note This statement loads the Timet::Application class, which is responsible for handling the command-line
6
+ # interface and user commands.
6
7
  require_relative 'timet/application'
7
8
 
8
9
  # Require the Timet::Database class from the 'timet/database' file.
@@ -12,5 +13,6 @@ require_relative 'timet/database'
12
13
 
13
14
  # Require the Timet::TimeReport class from the 'timet/time_report' file.
14
15
  #
15
- # @note This statement loads the Timet::TimeReport class, which is responsible for displaying a report of tracked time entries.
16
+ # @note This statement loads the Timet::TimeReport class, which is responsible for displaying a report
17
+ # of tracked time entries.
16
18
  require_relative 'timet/time_report'
Binary file
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: 1.2.0
4
+ version: 1.3.0
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-10-11 00:00:00.000000000 Z
11
+ date: 2024-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -86,8 +86,10 @@ files:
86
86
  - lib/timet/status_helper.rb
87
87
  - lib/timet/time_helper.rb
88
88
  - lib/timet/time_report.rb
89
+ - lib/timet/time_report_helper.rb
89
90
  - lib/timet/validation_edit_helper.rb
90
91
  - lib/timet/version.rb
92
+ - monthly_report.webp
91
93
  - sig/timet.rbs
92
94
  - timet.webp
93
95
  homepage: https://frankvielma.github.io/posts/timet-a-powerful-command-line-tool-for-tracking-your-time/