timet 1.5.6 → 1.5.7

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.
@@ -1,11 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'time_helper'
4
+ require_relative 'item_data_helper'
5
+ require_relative 'time_update_helper'
6
+ require_relative 'time_validation_helper'
7
+
4
8
  module Timet
5
9
  # Validates and updates a specific field of an item based on certain conditions.
6
10
  # If the field is 'start' or 'end', it checks and updates the value accordingly.
7
11
  # Otherwise, it directly updates the field with the new value.
8
12
  module ValidationEditHelper
13
+ include TimeValidationHelper
14
+
9
15
  # Constants for time fields.
10
16
  TIME_FIELDS = %w[start end].freeze
11
17
 
@@ -25,9 +31,9 @@ module Timet
25
31
  when 'tag'
26
32
  item[3] = new_value
27
33
  when 'start'
28
- item[1] = validate_time(new_value)
34
+ item[1] = validate_time(item, 'start', new_value)
29
35
  when 'end'
30
- item[2] = validate_time(new_value)
36
+ item[2] = validate_time(item, 'end', new_value)
31
37
  else
32
38
  raise ArgumentError, "Invalid field: #{field}"
33
39
  end
@@ -36,149 +42,119 @@ module Timet
36
42
 
37
43
  # Validates if a given time string is in a valid format.
38
44
  #
39
- # @param time_str [String] The time string to validate.
45
+ # @param item [Array] The item being modified.
46
+ # @param field [String] The field being validated ('start' or 'end').
47
+ # @param time_str [String, nil] The new time string (e.g., "HH:MM" or "HH:MM:SS").
48
+ # If nil or empty, it signifies that the original time should be kept.
40
49
  #
41
- # @return [Integer] The validated time as an integer.
50
+ # @return [Integer, nil] The validated time as an integer epoch.
51
+ # Returns the original timestamp for the field if time_str is nil/empty.
52
+ # Returns nil if the original field was nil and time_str is nil/empty.
42
53
  #
43
54
  # @raise [ArgumentError] If the time string is not in a valid format.
44
- def validate_time(time_str)
45
- Time.parse(time_str).to_i
46
- rescue ArgumentError
47
- raise ArgumentError, "Invalid time format: #{time_str}"
48
- end
55
+ def validate_time(item, field, time_str)
56
+ # If time_str is nil or empty, user pressed Enter, meaning no change to this field.
57
+ return field == 'start' ? item[1] : item[2] if time_str.nil? || time_str.strip.empty?
49
58
 
50
- private
59
+ parsed_time_component = parse_time_string(time_str)
51
60
 
52
- # Processes and updates a time field (start or end) of a tracking item.
53
- #
54
- # @param item [Array] The tracking item to be updated.
55
- # @param field [String] The time field to be updated.
56
- # @param date_value [String] The new value for the time field.
57
- # @param id [Integer] The ID of the tracking item.
58
- #
59
- # @return [void] This method does not return a value; it performs side effects such as updating the time field.
60
- #
61
- # @note The method formats the date value and checks if it is valid.
62
- # @note If the date value is valid, it updates the time field with the new value.
63
- # @note If the date value is invalid, it prints an error message.
64
- def process_and_update_time_field(*args)
65
- item, field, date_value, id = args
66
- formatted_date = TimeHelper.format_time_string(date_value)
61
+ start_timestamp = item[1]
62
+ end_timestamp = item[2]
67
63
 
68
- return print_error(date_value) unless formatted_date
64
+ new_datetime = determine_and_create_datetime(item, field, start_timestamp, parsed_time_component)
69
65
 
70
- new_date = update_time_field(item, field, formatted_date)
71
- new_value_epoch = new_date.to_i
66
+ new_epoch = new_datetime.to_i
72
67
 
73
- if valid_time_value?(item, field, new_value_epoch, id)
74
- @db.update_item(id, field, new_value_epoch)
75
- else
76
- print_error(new_date)
77
- end
78
- end
68
+ perform_validation(
69
+ item: item,
70
+ field: field,
71
+ new_epoch: new_epoch,
72
+ start_timestamp: start_timestamp,
73
+ end_timestamp: end_timestamp,
74
+ new_datetime: new_datetime
75
+ )
79
76
 
80
- # Prints an error message for an invalid date.
81
- #
82
- # @param message [String] The error message to be printed.
83
- #
84
- # @return [void] This method does not return a value; it performs side effects such as printing an error message.
85
- #
86
- # @example Print an error message for an invalid date
87
- # print_error('Invalid date: 2023-13-32')
88
- def print_error(message)
89
- puts "Invalid date: #{message}".red
77
+ new_epoch
90
78
  end
91
79
 
92
- # Updates a time field (start or end) of a tracking item with a formatted date value.
80
+ # Validates that the new start or end time does not collide with existing entries.
93
81
  #
94
- # @param item [Array] The tracking item to be updated.
95
- # @param field [String] The time field to be updated.
96
- # @param new_time [String] The new time value.
82
+ # @param item [Array] The item being modified.
83
+ # @param field [String] The field being validated ('start' or 'end').
84
+ # @param new_epoch [Integer] The new time in epoch format.
97
85
  #
98
- # @return [Time] The updated time value.
99
- #
100
- # @example Update the 'start' field of a tracking item with a formatted date value
101
- # update_time_field(item, 'start', '11:10:00')
102
- def update_time_field(item, field, new_time)
103
- field_index = Timet::Application::FIELD_INDEX[field]
104
- timestamp = item[field_index]
105
- edit_time = Time.at(timestamp || item[1]).to_s.split
106
- edit_time[1] = new_time
107
- DateTime.strptime(edit_time.join(' '), '%Y-%m-%d %H:%M:%S %z').to_time
108
- end
86
+ # @raise [ArgumentError] If the new time collides with a previous or next item.
87
+ def validate_time_collisions(item, field, new_epoch)
88
+ item_id = item[0]
89
+ prev_item = @db.find_item(item_id - 1)
90
+ next_item = @db.find_item(item_id + 1)
109
91
 
110
- # Validates if a new time value is valid for a specific time field (start or end).
111
- #
112
- # @param item [Array] The tracking item to be validated.
113
- # @param field [String] The time field to be validated.
114
- # @param new_value_epoch [Integer] The new time value in epoch format.
115
- # @param id [Integer] The ID of the tracking item.
116
- #
117
- # @return [Boolean] Returns true if the new time value is valid, otherwise false.
118
- #
119
- # @example Validate a new 'start' time value
120
- # valid_time_value?(item, 'start', 1633072800, 1)
121
- def valid_time_value?(*args)
122
- item, field, new_value_epoch, id = args
123
- item_start = fetch_item_start(item)
124
- item_end = fetch_item_end(item)
125
- item_before_end = fetch_item_before_end(id, item_start)
126
- item_after_start = fetch_item_after_start(id)
127
-
128
- if field == 'start'
129
- new_value_epoch.between?(item_before_end, item_end)
130
- else
131
- new_value_epoch.between?(item_start, item_after_start)
132
- end
92
+ check_collision_with_previous_item(field, new_epoch, prev_item)
93
+ check_collision_with_next_item(field, new_epoch, next_item)
133
94
  end
134
95
 
135
- # Fetches the start time of a tracking item.
136
- #
137
- # @param item [Array] The tracking item.
138
- #
139
- # @return [Integer] The start time in epoch format.
140
- #
141
- # @example Fetch the start time of a tracking item
142
- # fetch_item_start(item)
143
- def fetch_item_start(item)
144
- item[Timet::Application::FIELD_INDEX['start']]
96
+ # Checks for collision with the previous item.
97
+ def check_collision_with_previous_item(field, new_epoch, prev_item)
98
+ return unless prev_item && field == 'start' && new_epoch < prev_item[2]
99
+
100
+ raise ArgumentError,
101
+ 'New start time collides with previous item (ends at ' \
102
+ "#{Time.at(prev_item[2]).strftime('%Y-%m-%d %H:%M:%S')})."
145
103
  end
146
104
 
147
- # Fetches the end time of a tracking item.
148
- #
149
- # @param item [Array] The tracking item.
150
- #
151
- # @return [Integer] The end time in epoch format.
152
- #
153
- # @example Fetch the end time of a tracking item
154
- # fetch_item_end(item)
155
- def fetch_item_end(item)
156
- item[Timet::Application::FIELD_INDEX['end']] || TimeHelper.current_timestamp
105
+ # Checks for collision with the next item.
106
+ def check_collision_with_next_item(field, new_epoch, next_item)
107
+ return unless next_item
108
+
109
+ if field == 'start' && new_epoch > next_item[1]
110
+ raise ArgumentError,
111
+ 'New start time collides with next item (starts at ' \
112
+ "#{Time.at(next_item[1]).strftime('%Y-%m-%d %H:%M:%S')})."
113
+ elsif field == 'end' && new_epoch > next_item[1]
114
+ raise ArgumentError,
115
+ 'New end time collides with next item (starts at ' \
116
+ "#{Time.at(next_item[1]).strftime('%Y-%m-%d %H:%M:%S')})."
117
+ end
157
118
  end
158
119
 
159
- # Fetches the end time of the tracking item before the current one.
160
- #
161
- # @param id [Integer] The ID of the current tracking item.
162
- # @param item_start [Integer] The start time of the current tracking item.
120
+ private
121
+
122
+ # Determines the base date and time, creates a new datetime object, and adjusts it if necessary.
163
123
  #
164
- # @return [Integer] The end time of the previous tracking item in epoch format.
124
+ # @param item [Array] The item being modified.
125
+ # @param field [String] The field being validated ('start' or 'end').
126
+ # @param start_timestamp [Integer, nil] The start timestamp of the item.
127
+ # @param parsed_time_component [Time] The parsed time component.
165
128
  #
166
- # @example Fetch the end time of the previous tracking item
167
- # fetch_item_before_end(1, 1633072800)
168
- def fetch_item_before_end(id, item_start)
169
- @db.find_item(id - 1)&.dig(Timet::Application::FIELD_INDEX['end']) || item_start
129
+ # @return [Time] The new datetime object.
130
+ def determine_and_create_datetime(item, field, start_timestamp, parsed_time_component)
131
+ base_date_time = determine_base_date_time(item, field, start_timestamp)
132
+ new_datetime = create_new_datetime(base_date_time, parsed_time_component)
133
+ adjust_end_datetime(field, start_timestamp, new_datetime)
170
134
  end
171
135
 
172
- # Fetches the start time of the tracking item after the current one.
173
- #
174
- # @param id [Integer] The ID of the current tracking item.
175
- #
176
- # @return [Integer] The start time of the next tracking item in epoch format.
177
- #
178
- # @example Fetch the start time of the next tracking item
179
- # fetch_item_after_start(1)
180
- def fetch_item_after_start(id)
181
- @db.find_item(id + 1)&.dig(Timet::Application::FIELD_INDEX['start']) || TimeHelper.current_timestamp
136
+ # Performs the appropriate validation based on the field.
137
+ #
138
+ # @param options [Hash] A hash containing the parameters for validation.
139
+ # @option options [Array] :item The item being modified.
140
+ # @option options [String] :field The field being validated ('start' or 'end').
141
+ # @option options [Integer] :new_epoch The new time in epoch format.
142
+ # @option options [Integer, nil] :start_timestamp The start timestamp of the item.
143
+ # @option options [Integer, nil] :end_timestamp The end timestamp of the item.
144
+ # @option options [Time] :new_datetime The new datetime object.
145
+ def perform_validation(options)
146
+ item = options[:item]
147
+ field = options[:field]
148
+ new_epoch = options[:new_epoch]
149
+ start_timestamp = options[:start_timestamp]
150
+ end_timestamp = options[:end_timestamp]
151
+ new_datetime = options[:new_datetime]
152
+
153
+ validate_future_date(new_datetime)
154
+
155
+ validate_time_collisions(item, field, new_epoch) # Call the new method for both start and end
156
+ validate_start_time(new_epoch, end_timestamp, new_datetime) if field == 'start' && end_timestamp
157
+ validate_end_time(new_epoch, start_timestamp, new_datetime) if field == 'end'
182
158
  end
183
159
  end
184
160
  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.5.6'
10
- VERSION = '1.5.6'
9
+ # Timet::VERSION # => '1.5.7'
10
+ VERSION = '1.5.7'
11
11
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Timet
6
+ #
7
+ # The WeekInfo class encapsulates the date string and weeks array
8
+ # and provides methods for formatting and determining week information.
9
+ #
10
+ # It is instantiated for each date entry in the TimeBlockChart and helps decide
11
+ # how the week number is displayed and whether a separator line is needed before the entry.
12
+ class WeekInfo
13
+ # Initializes a new WeekInfo instance.
14
+ #
15
+ # @param date_object [Date] The Date object for the current entry.
16
+ # @param date_string_for_display [String] The original date string for display (e.g., "2023-10-01").
17
+ # @param weeks_array_ref [Array<Integer>] A reference to an array that accumulates the
18
+ # ISO 8601 week numbers of dates already processed. This array is mutated.
19
+
20
+ WEEKEND_DAYS = %w[Sat Sun].freeze
21
+ def initialize(date_object, date_string_for_display, weeks_array_ref)
22
+ @date_string = date_string_for_display # Use the passed string for display
23
+ @current_cweek = date_object.cweek
24
+
25
+ # Determine if a separator line should be printed *before* this entry.
26
+ # A separator is needed if this entry starts a new week group,
27
+ # and it's not the very first week group in the chart.
28
+ @print_separator_before_this = !weeks_array_ref.empty? && @current_cweek != weeks_array_ref.last
29
+
30
+ # Determine how the week number string should be displayed for this entry.
31
+ # It's underlined if it's the first time this cweek appears, otherwise blank.
32
+ is_first_display_of_this_cweek = weeks_array_ref.empty? || @current_cweek != weeks_array_ref.last
33
+ @week_display_string = if is_first_display_of_this_cweek
34
+ format('%02d', @current_cweek).underline
35
+ else
36
+ ' '
37
+ end
38
+
39
+ weeks_array_ref << @current_cweek # Record this week as processed
40
+ end
41
+
42
+ # Indicates whether an inter-week separator line should be printed before this date's entry.
43
+ #
44
+ # @return [Boolean] True if a separator is needed, false otherwise.
45
+ def needs_inter_week_separator?
46
+ @print_separator_before_this
47
+ end
48
+
49
+ # Formats and prints the date information
50
+ #
51
+ # @param [String] day The day of the week
52
+ # @return [void]
53
+ def format_and_print_date_info(day)
54
+ weekend_str = @date_string # Use the original date string for display
55
+ is_weekend_day = WEEKEND_DAYS.include?(day)
56
+ day_str = is_weekend_day ? day.red : day
57
+ weekend_str = weekend_str.red if is_weekend_day
58
+
59
+ print '┆'.gray + "#{@week_display_string} #{weekend_str} #{day_str}" + '┆- '.gray
60
+ end
61
+ end
62
+ 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.5.6
4
+ version: 1.5.7
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-04-07 00:00:00.000000000 Z
11
+ date: 2025-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -136,10 +136,12 @@ files:
136
136
  - lib/timet.rb
137
137
  - lib/timet/application.rb
138
138
  - lib/timet/application_helper.rb
139
+ - lib/timet/block_char_helper.rb
139
140
  - lib/timet/color_codes.rb
140
141
  - lib/timet/database.rb
141
142
  - lib/timet/database_sync_helper.rb
142
143
  - lib/timet/database_syncer.rb
144
+ - lib/timet/item_data_helper.rb
143
145
  - lib/timet/s3_supabase.rb
144
146
  - lib/timet/table.rb
145
147
  - lib/timet/tag_distribution.rb
@@ -148,8 +150,12 @@ files:
148
150
  - lib/timet/time_report.rb
149
151
  - lib/timet/time_report_helper.rb
150
152
  - lib/timet/time_statistics.rb
153
+ - lib/timet/time_update_helper.rb
154
+ - lib/timet/time_validation_helper.rb
155
+ - lib/timet/utils.rb
151
156
  - lib/timet/validation_edit_helper.rb
152
157
  - lib/timet/version.rb
158
+ - lib/timet/week_info.rb
153
159
  - sig/timet.rbs
154
160
  homepage: https://frankvielma.github.io/posts/timet-a-powerful-command-line-tool-for-tracking-your-time/
155
161
  licenses: