timet 1.2.1 → 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: db3c3e635b08a071bc271be5788a4b689f0a28203dea81c2dc73a78c312b089e
4
- data.tar.gz: 2c5c831731c0ec619064653b8239381a53e5c973f92f347e98a74de8c07547e0
3
+ metadata.gz: 822542486f30f170aa48dccd0577637caed0010047b085022653f3ae4d0cb848
4
+ data.tar.gz: 96b166cdd9ac9fe69ee546fec4d68e47d113df05f986b4aff61cc71358c1c9ea
5
5
  SHA512:
6
- metadata.gz: 0b98ea65edc254804e58c16ce0f200209ee57b9b3629fc182ec822c1b359380015bdaf281db64a9bd96acaa5ded45489f4464602ae90ab3e11916556e879e88a
7
- data.tar.gz: fba083a2484a17a3af3efb5c0d2dcad4b08f463d162b6689c4a6445948d6f2bd517ef0b3efd794d0b0b05a818799798446a65902cf97ebe94519f884aa2c9927
6
+ metadata.gz: 0c950e2ff135ba0f665a70960c0389a55ac83ccf863dbdcee219d83d4fbc1939b79411c6e549e082f8b0abc1e9df223079dc5f784bf466f18a8ec9a7d52be039
7
+ data.tar.gz: 4d76b845a5b3af8ad142e8a1edb65309f9b637b755af129401f7fdcfcc6635c1deabb37665302649fc3eaa3f4cf73f2212114111bc594dbdf6e62b5d1956c7ed
data/CHANGELOG.md CHANGED
@@ -1,18 +1,66 @@
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
+
3
48
  ## [1.2.1] - 2024-10-18
4
49
 
5
50
  **Improvements:**
51
+
6
52
  - Updated the time block chart formatting to use square brackets for better visual representation.
7
53
  - Refactored the `play_sound_and_notify` method to avoid redundant platform checks and introduced platform-specific session runners.
8
54
  - Improved readability and maintainability of the `format_tag_distribution` method by extracting logic into a new private method.
9
55
  - Updated the `rubocop` gem from `~> 1.65` to `~> 1.67`.
10
56
 
11
57
  ### Bug fixes:
58
+
12
59
  - Fixed a `NoMethodError` caused by an undefined method `process_and_print_tags` in the `format_tag_distribution` method.
13
60
  - Fixed line length violations in several files to comply with `rubocop` rules.
14
61
 
15
62
  ### Additional Considerations:
63
+
16
64
  - The changes in this pull request should be thoroughly tested to ensure that they do not introduce any regressions.
17
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.
18
66
 
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:
@@ -42,6 +42,8 @@ Tracked time report [today]:
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)
@@ -4,6 +4,18 @@ 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
21
  # @return [void] This method does not return a value; it performs side effects such as printing
@@ -61,9 +73,10 @@ module Timet
61
73
  # @note The method truncates the notes to a maximum of 20 characters and pads them to a fixed width.
62
74
  def format_notes(notes)
63
75
  spaces = 17
64
- return ' ' * spaces if notes.nil?
76
+ return ' ' * spaces unless notes
65
77
 
66
- notes = "#{notes.slice(0, spaces - 3)}..." if notes.length > spaces - 3
78
+ max_length = spaces - 3
79
+ notes = "#{notes.slice(0, max_length)}..." if notes.length > max_length
67
80
  notes.ljust(spaces)
68
81
  end
69
82
 
@@ -79,13 +92,13 @@ module Timet
79
92
  #
80
93
  # @param duration_by_tag [Hash<String, Integer>] A hash where keys are tags and values are durations in seconds.
81
94
  # @return [void] This method outputs the formatted tag distribution to the console.
82
- def format_tag_distribution(duration_by_tag)
95
+ def format_tag_distribution(duration_by_tag, colors)
83
96
  total = duration_by_tag.values.sum
84
97
  return unless total.positive?
85
98
 
86
99
  factor = duration_by_tag.size < 3 ? 2 : 1
87
100
  sorted_duration_by_tag = duration_by_tag.sort_by { |_, duration| -duration }
88
- process_and_print_tags(sorted_duration_by_tag, factor, total)
101
+ process_and_print_tags(sorted_duration_by_tag, factor, total, colors)
89
102
  end
90
103
 
91
104
  # Processes and prints the tag distribution information.
@@ -95,55 +108,78 @@ module Timet
95
108
  # @param factor [Numeric] The factor used to adjust the bar length.
96
109
  # @param total [Numeric] The total duration of all tags combined.
97
110
  # @return [void] This method outputs the tag distribution information to the standard output.
98
- def process_and_print_tags(sorted_duration_by_tag, factor, total)
111
+ def process_and_print_tags(*args)
112
+ sorted_duration_by_tag, factor, total, colors = args
99
113
  block = '▅'
100
114
  sorted_duration_by_tag.each do |tag, duration|
101
- value = (duration.to_f / total * 100).round(2)
102
- bar_length = (value / factor).to_i
103
- color = rand(256)
104
- puts "#{tag.rjust(8)}: #{value.to_s.rjust(7)}% \u001b[38;5;#{color}m#{block * bar_length}\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"
105
117
  end
106
118
  end
107
119
 
108
- # Prints the entire time block chart.
120
+ # Calculates the value and bar length for a given duration, total duration, and factor.
109
121
  #
110
- # This method orchestrates the printing of the entire time block chart by calling
111
- # the `print_header` and `print_blocks` methods. It also prints the separator line
112
- # between the header and the blocks, and adds a double newline at the end for
113
- # 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.
114
126
  #
115
- # @param time_block [Hash] A hash where the keys are formatted hour strings
116
- # (e.g., "00", "01") and the values are the corresponding
117
- # values to determine the block character.
118
127
  # @example
119
- # time_block = { "00" => 100, "01" => 200, ..., "23" => 300 }
120
- # print_time_block_chart(time_block)
121
- # # Output:
122
- # # [ 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ]
123
- # # [ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █
124
- # #
125
- # # (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" }
143
+ #
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)
126
150
  #
127
- def print_time_block_chart(time_block)
128
- print_header
129
- print ' [ '
130
- print_blocks(time_block)
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)
131
160
  end
132
161
 
133
162
  # Prints the header of the time block chart.
134
163
  #
135
- # This method outputs the header line of the chart, which includes the hours
136
- # 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.
137
170
  #
138
171
  # @example
139
- # print_header
172
+ # print_header(10)
140
173
  # # Output:
141
- # # ⏳ ↦ [ 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]
142
176
  #
143
- 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)
144
180
  puts
145
- print '⏳ ↦ [ '
146
- (0..23).each { |hour| print format('%02d', hour).ljust(4) }
181
+ print ' ⏳ ↦ [ '
182
+ (start_time..23).each { |hour| print format('%02d', hour).ljust(4) }
147
183
  print ']'
148
184
  puts
149
185
  end
@@ -165,15 +201,72 @@ module Timet
165
201
  # #
166
202
  # # (followed by two newlines)
167
203
  #
168
- def print_blocks(time_block)
204
+ def print_blocks(time_block, colors, start_time)
169
205
  return unless time_block
170
206
 
171
- (0..23).each do |hour|
172
- block_char = get_block_char(time_block[format('%02d', hour)])
173
- 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
174
212
  end
175
- print ']'
176
- puts "\n\n"
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)
234
+ end
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)
177
270
  end
178
271
 
179
272
  # Determines the block character based on the value.
@@ -181,19 +274,9 @@ module Timet
181
274
  # @param value [Integer] The value to determine the block character for.
182
275
  # @return [String] The block character corresponding to the value.
183
276
  def get_block_char(value)
184
- range_to_char = {
185
- 0..120 => ' ',
186
- 121..450 => '',
187
- 451..900 => '▂',
188
- 901..1350 => '▃',
189
- 1351..1800 => '▄',
190
- 1801..2250 => '▅',
191
- 2251..2700 => '▆',
192
- 2701..3150 => '▇',
193
- 3151..3600 => '█'
194
- }
195
-
196
- range_to_char.find { |range, _| range.include?(value) }&.last || ' '
277
+ return ' ' unless value
278
+
279
+ CHAR_MAPPING.find { |range, _| range.include?(value) }&.last || ' '
197
280
  end
198
281
  end
199
282
  end
@@ -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
@@ -53,23 +55,14 @@ module Timet
53
55
  return puts 'No tracked time found for the specified filter.' if items.empty?
54
56
 
55
57
  format_table_header
56
- duration_by_tag = Hash.new(0)
57
- time_block = []
58
- items.each_with_index do |item, idx|
59
- date = TimeHelper.extract_date(items, idx)
60
- display_time_entry(item, date)
61
- time_block << TimeHelper.count_seconds_per_hour_block(item[1], item[2])
62
- duration_by_tag[item[3]] += TimeHelper.calculate_duration(item[1], item[2])
63
- end
58
+ time_block, duration_by_tag = process_time_entries
64
59
  puts format_table_separator
65
60
  total
66
61
 
67
- if Time.now.to_i - items.map { |x| x[1] }.min < 86_400
68
- time_block_reverse = TimeHelper.aggregate_hash_values(time_block)
69
- print_time_block_chart(time_block_reverse)
70
- end
62
+ colors = duration_by_tag.map { |x| x[0] }.sort.each_with_index.to_h
63
+ print_time_block_chart(time_block, colors)
71
64
 
72
- format_tag_distribution(duration_by_tag)
65
+ format_tag_distribution(duration_by_tag, colors)
73
66
  end
74
67
 
75
68
  # Displays a single row of the report.
@@ -119,33 +112,10 @@ module Timet
119
112
  def write_csv(file_name)
120
113
  CSV.open(file_name, 'w') do |csv|
121
114
  csv << %w[ID Start End Tag Notes]
122
- items.each do |item|
123
- csv << format_item(item)
124
- end
115
+ write_csv_rows(csv)
125
116
  end
126
117
  end
127
118
 
128
- # Formats an item for CSV export.
129
- #
130
- # @param item [Array] The item to format.
131
- #
132
- # @return [Array] The formatted item.
133
- #
134
- # @example Format an item for CSV export
135
- # format_item(item)
136
- #
137
- # @note The method formats the item's ID, start time, end time, tag, and notes.
138
- def format_item(item)
139
- id, start_time, end_time, tags, notes = item
140
- [
141
- id,
142
- TimeHelper.format_time(start_time),
143
- TimeHelper.format_time(end_time),
144
- tags,
145
- notes
146
- ]
147
- end
148
-
149
119
  # Displays a single time entry in the report.
150
120
  #
151
121
  # @param item [Array] The item to display.
@@ -180,7 +150,7 @@ module Timet
180
150
  total = @items.map do |item|
181
151
  TimeHelper.calculate_duration(item[1], item[2])
182
152
  end.sum
183
- puts "|#{' ' * 37}\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 |"
184
154
  puts format_table_separator
185
155
  end
186
156
 
@@ -208,24 +178,6 @@ module Timet
208
178
  end
209
179
  end
210
180
 
211
- # Provides predefined date ranges for filtering.
212
- #
213
- # @return [Hash] A hash containing predefined date ranges.
214
- #
215
- # @example Get the predefined date ranges
216
- # date_ranges
217
- #
218
- # @note The method returns a hash with predefined date ranges for 'today', 'yesterday', 'week', and 'month'.
219
- def date_ranges
220
- today = Date.today
221
- {
222
- 'today' => [today, nil],
223
- 'yesterday' => [today - 1, nil],
224
- 'week' => [today - 7, today + 1],
225
- 'month' => [today - 30, today + 1]
226
- }
227
- end
228
-
229
181
  # Filters the items by date range and tag.
230
182
  #
231
183
  # @param start_date [Date] The start date of the range.
@@ -273,22 +225,5 @@ module Timet
273
225
 
274
226
  'today'
275
227
  end
276
-
277
- # Validates the date format.
278
- #
279
- # @param date_string [String] The date string to validate.
280
- #
281
- # @return [Boolean] True if the date format is valid, otherwise false.
282
- #
283
- # @example Validate the date format
284
- # valid_date_format?('2021-10-01') # => true
285
- #
286
- # @note The method validates the date format for single dates and date ranges.
287
- def valid_date_format?(date_string)
288
- date_format_single = /^\d{4}-\d{2}-\d{2}$/
289
- date_format_range = /^\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}$/
290
-
291
- date_string.match?(date_format_single) || date_string.match?(date_format_range)
292
- end
293
228
  end
294
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.1'
10
- VERSION = '1.2.1'
9
+ # Timet::VERSION # => '1.3.0'
10
+ VERSION = '1.3.0'
11
11
  end
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.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-18 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/