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 +4 -4
- data/CHANGELOG.md +85 -1
- data/README.md +6 -4
- data/lib/timet/application.rb +48 -24
- data/lib/timet/application_helper.rb +37 -10
- data/lib/timet/database.rb +20 -10
- data/lib/timet/formatter.rb +154 -55
- data/lib/timet/status_helper.rb +2 -1
- data/lib/timet/time_helper.rb +35 -37
- data/lib/timet/time_report.rb +12 -75
- data/lib/timet/time_report_helper.rb +150 -0
- data/lib/timet/version.rb +2 -2
- data/lib/timet.rb +4 -2
- data/monthly_report.webp +0 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 822542486f30f170aa48dccd0577637caed0010047b085022653f3ae4d0cb848
|
4
|
+
data.tar.gz: 96b166cdd9ac9fe69ee546fec4d68e47d113df05f986b4aff61cc71358c1c9ea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
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
|
-
⏳ ↦
|
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
|
+

|
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
|

|
data/lib/timet/application.rb
CHANGED
@@ -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
|
49
|
-
#
|
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,
|
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
|
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
|
84
|
-
#
|
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
|
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,
|
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
|
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',
|
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
|
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
|
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
|
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'.
|
163
|
-
#
|
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
|
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
|
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
|
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
|
205
|
-
#
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
elsif
|
96
|
-
|
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
|
data/lib/timet/database.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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)
|
data/lib/timet/formatter.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
75
|
+
spaces = 17
|
76
|
+
return ' ' * spaces unless notes
|
63
77
|
|
64
|
-
|
65
|
-
notes.
|
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
|
90
|
-
puts "#{tag.rjust(8)}: #{value.to_s.rjust(7)}% \u001b[38;5;#{
|
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
|
-
#
|
120
|
+
# Calculates the value and bar length for a given duration, total duration, and factor.
|
95
121
|
#
|
96
|
-
#
|
97
|
-
#
|
98
|
-
#
|
99
|
-
#
|
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
|
-
#
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
#
|
122
|
-
#
|
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
|
-
# #
|
174
|
+
# #
|
175
|
+
# # ⏳ ↦ [ 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
|
128
176
|
#
|
129
|
-
|
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
|
-
(
|
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
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
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
|
data/lib/timet/status_helper.rb
CHANGED
@@ -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
|
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
|
data/lib/timet/time_helper.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
-
|
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
|
-
#
|
212
|
-
#
|
213
|
-
#
|
214
|
-
#
|
215
|
-
#
|
216
|
-
#
|
217
|
-
#
|
218
|
-
# @return [Hash]
|
219
|
-
#
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
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
|
data/lib/timet/time_report.rb
CHANGED
@@ -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',
|
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
|
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 =
|
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
|
-
|
66
|
-
|
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
|
-
|
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
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
|
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
|
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'
|
data/monthly_report.webp
ADDED
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.
|
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
|
+
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/
|