timet 1.6.1.1 → 1.6.3

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.
@@ -0,0 +1,303 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require_relative 'time_helper'
5
+
6
+ module Timet
7
+ # Helper module containing validation logic for time tracking items.
8
+ module TimeValidationHelpers
9
+ module_function
10
+
11
+ def adjust_end_datetime_for_next_day(start_timestamp, new_datetime)
12
+ return new_datetime unless start_timestamp && (new_datetime.to_i <= start_timestamp)
13
+
14
+ new_datetime + (24 * 60 * 60)
15
+ end
16
+
17
+ def create_new_datetime(base_date_time, parsed_time_component)
18
+ Time.new(
19
+ base_date_time.year,
20
+ base_date_time.month,
21
+ base_date_time.day,
22
+ parsed_time_component.hour,
23
+ parsed_time_component.min,
24
+ parsed_time_component.sec,
25
+ base_date_time.utc_offset
26
+ )
27
+ end
28
+
29
+ def determine_start_base_date_time(start_timestamp)
30
+ start_timestamp ? Time.at(start_timestamp) : Time.now
31
+ end
32
+
33
+ def determine_end_base_date_time(start_timestamp)
34
+ raise ArgumentError, "Cannot set 'end' time because 'start' time is not set." unless start_timestamp
35
+
36
+ Time.at(start_timestamp)
37
+ end
38
+
39
+ def determine_base_date_time(_item, field, start_timestamp)
40
+ case field
41
+ when 'start'
42
+ determine_start_base_date_time(start_timestamp)
43
+ when 'end'
44
+ determine_end_base_date_time(start_timestamp)
45
+ else
46
+ raise ArgumentError, "Invalid field: #{field}"
47
+ end
48
+ end
49
+
50
+ def parse_time_string(time_str)
51
+ Time.parse(time_str)
52
+ rescue ArgumentError
53
+ raise ArgumentError, "Invalid time format: #{time_str}"
54
+ end
55
+
56
+ def format_time(epoch)
57
+ Time.at(epoch).strftime('%Y-%m-%d %H:%M:%S')
58
+ end
59
+
60
+ def validate_future_date(new_datetime)
61
+ return unless new_datetime > Time.now.getlocal
62
+
63
+ raise ArgumentError, "Cannot set time to a future date or time: #{new_datetime.strftime('%Y-%m-%d %H:%M:%S')}"
64
+ end
65
+
66
+ def validate_time_difference(earlier_timestamp, later_timestamp)
67
+ return unless (later_timestamp - earlier_timestamp).abs >= 24 * 60 * 60
68
+
69
+ raise ArgumentError, 'The difference between start and end time must be less than 24 hours.'
70
+ end
71
+
72
+ def validate_end_after_start(new_epoch, ref_timestamp, new_datetime)
73
+ reference_time = Time.at(ref_timestamp)
74
+ formatted_new = new_datetime.strftime('%Y-%m-%d %H:%M:%S')
75
+ formatted_ref = reference_time.strftime('%Y-%m-%d %H:%M:%S')
76
+
77
+ return unless new_epoch <= ref_timestamp
78
+
79
+ raise ArgumentError, "End time (#{formatted_new}) must be after start time (#{formatted_ref})."
80
+ end
81
+
82
+ def validate_start_before_end(new_epoch, ref_timestamp, new_datetime)
83
+ reference_time = Time.at(ref_timestamp)
84
+ formatted_new = new_datetime.strftime('%Y-%m-%d %H:%M:%S')
85
+ formatted_ref = reference_time.strftime('%Y-%m-%d %H:%M:%S')
86
+
87
+ return unless new_epoch >= ref_timestamp
88
+
89
+ raise ArgumentError, "Start time (#{formatted_new}) must be before end time (#{formatted_ref})."
90
+ end
91
+
92
+ def validate_end_time(new_epoch, ref_timestamp, new_datetime)
93
+ validate_end_after_start(new_epoch, ref_timestamp, new_datetime)
94
+ validate_time_difference(ref_timestamp, new_epoch)
95
+ end
96
+
97
+ def check_start_before_end(new_epoch, ref_timestamp, new_datetime)
98
+ validate_start_before_end(new_epoch, ref_timestamp, new_datetime)
99
+ validate_time_difference(new_epoch, ref_timestamp)
100
+ end
101
+
102
+ def fetch_item_start(item)
103
+ item[Timet::Application::FIELD_INDEX['start']]
104
+ end
105
+
106
+ def fetch_item_end(item)
107
+ item[Timet::Application::FIELD_INDEX['end']] || TimeHelper.current_timestamp
108
+ end
109
+
110
+ def fetch_item_before_end(db, id, item_start)
111
+ db.find_item(id - 1)&.dig(Timet::Application::FIELD_INDEX['end']) || item_start
112
+ end
113
+
114
+ def fetch_item_after_start(db, id)
115
+ db.find_item(id + 1)&.dig(Timet::Application::FIELD_INDEX['start']) || TimeHelper.current_timestamp
116
+ end
117
+ end
118
+
119
+ # Handles validation and editing of time tracking items.
120
+ class ValidationEditor
121
+ include TimeValidationHelpers
122
+
123
+ TIME_FIELDS = %w[start end].freeze
124
+
125
+ def initialize(item, db)
126
+ @item = item
127
+ @db = db
128
+ end
129
+
130
+ def update(field, new_value)
131
+ case field
132
+ when 'notes' then update_notes(new_value)
133
+ when 'tag' then update_tag(new_value)
134
+ when 'start' then update_start_time(new_value)
135
+ when 'end' then update_end_time(new_value)
136
+ else raise ArgumentError, "Invalid field: #{field}"
137
+ end
138
+ @item
139
+ end
140
+
141
+ private
142
+
143
+ def update_notes(value)
144
+ @item[4] = value
145
+ end
146
+
147
+ def update_tag(value)
148
+ @item[3] = value
149
+ end
150
+
151
+ def update_start_time(time_str)
152
+ @item[1] = process_start_time(time_str)
153
+ end
154
+
155
+ def update_end_time(time_str)
156
+ @item[2] = process_end_time(time_str)
157
+ end
158
+
159
+ def process_start_time(time_str)
160
+ return @item[1] if time_str.to_s.strip.empty?
161
+
162
+ build_and_validate_start_time(time_str)
163
+ end
164
+
165
+ def process_end_time(time_str)
166
+ return @item[2] if time_str.to_s.strip.empty?
167
+
168
+ build_and_validate_end_time(time_str)
169
+ end
170
+
171
+ def start_timestamp
172
+ @item[1]
173
+ end
174
+
175
+ def end_timestamp
176
+ @item[2]
177
+ end
178
+
179
+ def item_id
180
+ @item[0]
181
+ end
182
+
183
+ def build_and_validate_start_time(time_str)
184
+ new_epoch = parse_to_epoch(time_str, :start)
185
+ run_start_validations(new_epoch)
186
+ new_epoch
187
+ end
188
+
189
+ def build_and_validate_end_time(time_str)
190
+ new_epoch = parse_to_epoch(time_str, :end)
191
+ run_end_validations(new_epoch)
192
+ new_epoch
193
+ end
194
+
195
+ def parse_to_epoch(time_str, field_type)
196
+ parsed_time = parse_time_string(time_str)
197
+ base = determine_base_date_time(@item, field_type.to_s, start_timestamp)
198
+ new_dt = create_new_datetime(base, parsed_time)
199
+ adjusted_dt = field_type == :end ? adjust_end_datetime_for_next_day(start_timestamp, new_dt) : new_dt
200
+ adjusted_dt.to_i
201
+ end
202
+
203
+ def run_start_validations(new_epoch)
204
+ validate_start_not_future(new_epoch)
205
+ validate_start_collision_with_previous(new_epoch)
206
+ validate_start_collision_with_next(new_epoch)
207
+ validate_start_before_item_end(new_epoch)
208
+ end
209
+
210
+ def run_end_validations(new_epoch)
211
+ validate_end_not_future(new_epoch)
212
+ validate_end_collision_with_previous(new_epoch)
213
+ validate_end_collision_with_next(new_epoch)
214
+ validate_end_after_start_item(new_epoch)
215
+ end
216
+
217
+ def validate_start_not_future(new_epoch)
218
+ validate_future_date(Time.at(new_epoch))
219
+ end
220
+
221
+ def validate_end_not_future(new_epoch)
222
+ validate_future_date(Time.at(new_epoch))
223
+ end
224
+
225
+ def validate_start_collision_with_previous(new_epoch)
226
+ prev = @db.find_item(item_id - 1)
227
+ return unless prev
228
+
229
+ prev_end = prev[2]
230
+ return unless new_epoch < prev_end
231
+
232
+ raise ArgumentError, "New start time collides with previous item (ends at #{format_time(prev_end)})."
233
+ end
234
+
235
+ def validate_end_collision_with_previous(new_epoch)
236
+ prev = @db.find_item(item_id - 1)
237
+ return unless prev
238
+
239
+ prev_end = prev[2]
240
+ return unless new_epoch < prev_end
241
+
242
+ raise ArgumentError, "New end time collides with previous item (ends at #{format_time(prev_end)})."
243
+ end
244
+
245
+ def validate_start_collision_with_next(new_epoch)
246
+ next_item = @db.find_item(item_id + 1)
247
+ return unless next_item
248
+
249
+ next_start = next_item[1]
250
+ return unless new_epoch >= next_start
251
+
252
+ raise ArgumentError, "New start time collides with next item (starts at #{format_time(next_start)})."
253
+ end
254
+
255
+ def validate_end_collision_with_next(new_epoch)
256
+ next_item = @db.find_item(item_id + 1)
257
+ return unless next_item
258
+
259
+ next_start = next_item[1]
260
+ return unless new_epoch > next_start
261
+
262
+ raise ArgumentError, "New end time collides with next item (starts at #{format_time(next_start)})."
263
+ end
264
+
265
+ def validate_start_before_item_end(new_epoch)
266
+ end_ts = end_timestamp
267
+ return unless end_ts
268
+
269
+ check_start_before_end(new_epoch, end_ts, Time.at(new_epoch))
270
+ end
271
+
272
+ def validate_end_after_start_item(new_epoch)
273
+ start_ts = start_timestamp
274
+ validate_end_time(new_epoch, start_ts, Time.at(new_epoch))
275
+ end
276
+
277
+ def process_and_update_time_field(item, field, date_value, id)
278
+ formatted_date = TimeHelper.format_time_string(date_value)
279
+
280
+ return print_error(date_value) unless formatted_date
281
+
282
+ new_date = TimeHelper.update_time_field(item, field, formatted_date)
283
+ new_value_epoch = new_date.to_i
284
+
285
+ return @db.update_item(id, field, new_value_epoch) unless invalid_time_value?(item, field, new_value_epoch, id)
286
+
287
+ print_error(new_date)
288
+ end
289
+
290
+ def print_error(message)
291
+ puts "Invalid date: #{message}".red
292
+ end
293
+
294
+ def invalid_time_value?(item, field, new_value_epoch, id)
295
+ is_start = field == 'start'
296
+ item_start = fetch_item_start(item)
297
+ item_end = fetch_item_end(item)
298
+ min_val = is_start ? fetch_item_before_end(@db, id, item_start) : item_start
299
+ max_val = is_start ? item_end : fetch_item_after_start(@db, id)
300
+ !new_value_epoch.between?(min_val, max_val)
301
+ end
302
+ end
303
+ 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.6.1.1'
10
- VERSION = '1.6.1.1'
9
+ # Timet::VERSION # => '1.6.3'
10
+ VERSION = '1.6.3'
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timet
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.1.1
4
+ version: 1.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frank Vielma
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-10 00:00:00.000000000 Z
11
+ date: 2026-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -106,14 +106,14 @@ dependencies:
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: '1.178'
109
+ version: '1.209'
110
110
  type: :runtime
111
111
  prerelease: false
112
112
  version_requirements: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: '1.178'
116
+ version: '1.209'
117
117
  description: Timet is a command-line time tracker that keeps track of your activities.
118
118
  email:
119
119
  - frankvielma@gmail.com
@@ -124,6 +124,7 @@ extensions: []
124
124
  extra_rdoc_files: []
125
125
  files:
126
126
  - ".deepsource.toml"
127
+ - ".qlty/qlty.toml"
127
128
  - ".reek.yml"
128
129
  - ".rspec"
129
130
  - ".rubocop.yml"
@@ -143,19 +144,15 @@ files:
143
144
  - lib/timet/database_sync_helper.rb
144
145
  - lib/timet/database_syncer.rb
145
146
  - lib/timet/discord_notifier.rb
146
- - lib/timet/item_data_helper.rb
147
147
  - lib/timet/s3_supabase.rb
148
148
  - lib/timet/table.rb
149
149
  - lib/timet/tag_distribution.rb
150
150
  - lib/timet/time_block_chart.rb
151
151
  - lib/timet/time_helper.rb
152
152
  - lib/timet/time_report.rb
153
- - lib/timet/time_report_helper.rb
154
153
  - lib/timet/time_statistics.rb
155
- - lib/timet/time_update_helper.rb
156
- - lib/timet/time_validation_helper.rb
157
154
  - lib/timet/utils.rb
158
- - lib/timet/validation_edit_helper.rb
155
+ - lib/timet/validation_editor.rb
159
156
  - lib/timet/version.rb
160
157
  - lib/timet/week_info.rb
161
158
  - sig/timet.rbs
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Timet
4
- # Helper methods for fetching item data.
5
- module ItemDataHelper
6
- module_function
7
-
8
- # Fetches the start time of a tracking item.
9
- #
10
- # @param item [Array] The tracking item.
11
- #
12
- # @return [Integer] The start time in epoch format.
13
- #
14
- # @example Fetch the start time of a tracking item
15
- # fetch_item_start(item)
16
- def fetch_item_start(item)
17
- item[Timet::Application::FIELD_INDEX['start']]
18
- end
19
-
20
- # Fetches the end time of a tracking item.
21
- #
22
- # @param item [Array] The tracking item.
23
- #
24
- # @return [Integer] The end time in epoch format.
25
- #
26
- # @example Fetch the end time of a tracking item
27
- # fetch_item_end(item)
28
- def fetch_item_end(item)
29
- item[Timet::Application::FIELD_INDEX['end']] || TimeHelper.current_timestamp
30
- end
31
-
32
- # Fetches the end time of the tracking item before the current one.
33
- #
34
- # @param db [Timet::Database] The database instance.
35
- # @param id [Integer] The ID of the current tracking item.
36
- # @param item_start [Integer] The start time of the current tracking item.
37
- #
38
- # @return [Integer] The end time of the previous tracking item in epoch format.
39
- #
40
- # @example Fetch the end time of the previous tracking item
41
- # fetch_item_before_end(db, 1, 1633072800)
42
- def fetch_item_before_end(db, id, item_start)
43
- db.find_item(id - 1)&.dig(Timet::Application::FIELD_INDEX['end']) || item_start
44
- end
45
-
46
- # Fetches the start time of the tracking item after the current one.
47
- #
48
- # @param db [Timet::Database] The database instance.
49
- # @param id [Integer] The ID of the current tracking item.
50
- #
51
- # @return [Integer] The start time of the next tracking item in epoch format.
52
- #
53
- # @example Fetch the start time of the next tracking item
54
- # fetch_item_after_start(db, id)
55
- def fetch_item_after_start(db, id)
56
- db.find_item(id + 1)&.dig(Timet::Application::FIELD_INDEX['start']) || TimeHelper.current_timestamp
57
- end
58
- end
59
- end
@@ -1,36 +0,0 @@
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
- # Exports the report to a CSV file.
10
- #
11
- # @return [void] This method does not return a value; it performs side effects such as writing the CSV file.
12
- #
13
- # @example Export the report to a CSV file
14
- # time_report.export_csv
15
- #
16
- # @note The method writes the items to a CSV file and prints a confirmation message.
17
- def export_csv
18
- file_name = "#{csv_filename}.csv"
19
- write_csv(file_name)
20
-
21
- puts "The #{file_name} has been exported."
22
- end
23
-
24
- # Generates an iCalendar file and writes it to disk.
25
- #
26
- # @return [void]
27
- def export_icalendar
28
- file_name = "#{ics_filename}.ics"
29
- cal = Timet::Utils.add_events(items)
30
-
31
- File.write(file_name, cal.to_ical)
32
-
33
- puts "The #{file_name} has been generated."
34
- end
35
- end
36
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'time_helper'
4
- require_relative 'item_data_helper'
5
- require 'date'
6
-
7
- module Timet
8
- # Helper methods for processing and updating time fields.
9
- module TimeUpdateHelper
10
- # Processes and updates a time field (start or end) of a tracking item.
11
- #
12
- # @param item [Array] The tracking item to be updated.
13
- # @param field [String] The time field to be updated.
14
- # @param date_value [String] The new value for the time field.
15
- # @param id [Integer] The ID of the tracking item.
16
- #
17
- # @return [void] This method does not return a value; it performs side effects such as updating the time field.
18
- #
19
- # @note The method formats the date value and checks if it is valid.
20
- # @note If the date value is valid, it updates the time field with the new value.
21
- # @note If the date value is invalid, it prints an error message.
22
- def process_and_update_time_field(*args)
23
- item, field, date_value, id = args
24
- formatted_date = TimeHelper.format_time_string(date_value)
25
-
26
- return print_error(date_value) unless formatted_date
27
-
28
- new_date = TimeHelper.update_time_field(item, field, formatted_date)
29
- new_value_epoch = new_date.to_i
30
-
31
- if valid_time_value?(item, field, new_value_epoch, id)
32
- @db.update_item(id, field, new_value_epoch)
33
- else
34
- print_error(new_date)
35
- end
36
- end
37
-
38
- # Prints an error message for an invalid date.
39
- #
40
- # @param message [String] The error message to be printed.
41
- #
42
- # @return [void] This method does not return a value; it performs side effects such as printing an error message.
43
- #
44
- # @example Print an error message for an invalid date
45
- # print_error('Invalid date: 2023-13-32')
46
- def print_error(message)
47
- puts "Invalid date: #{message}".red
48
- end
49
-
50
- # Validates if a new time value is valid for a specific time field (start or end).
51
- #
52
- # @param item [Array] The tracking item to be validated.
53
- # @param field [String] The time field to be validated.
54
- # @param new_value_epoch [Integer] The new time value in epoch format.
55
- # @param id [Integer] The ID of the tracking item.
56
- #
57
- # @return [Boolean] Returns true if the new time value is valid, otherwise false.
58
- #
59
- # @example Validate a new 'start' time value
60
- # valid_time_value?(item, 'start', 1633072800, 1)
61
- def valid_time_value?(*args)
62
- item, field, new_value_epoch, id = args
63
- item_start = ItemDataHelper.fetch_item_start(item)
64
- item_end = ItemDataHelper.fetch_item_end(item)
65
- item_before_end = ItemDataHelper.fetch_item_before_end(@db, id, item_start)
66
- item_after_start = ItemDataHelper.fetch_item_after_start(@db, id)
67
-
68
- if field == 'start'
69
- new_value_epoch.between?(item_before_end, item_end)
70
- else
71
- new_value_epoch.between?(item_start, item_after_start)
72
- end
73
- end
74
- end
75
- end