timet 1.5.5 → 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,174 +1,160 @@
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
 
12
- # Validates and updates a tracking item's field with a new value.
18
+ # Validates and updates an item's attribute based on the provided field and new value.
13
19
  #
14
- # @param item [Array] The tracking item to be updated.
20
+ # @param item [Array] The item to be updated.
15
21
  # @param field [String] The field to be updated.
16
- # @param new_value [String, nil] The new value to be set for the specified field.
17
- #
18
- # @return [Array, nil] The updated tracking item if the update was successful, otherwise nil.
22
+ # @param new_value [String] The new value for the field.
19
23
  #
20
- # @example Validate and update the 'notes' field of a tracking item
21
- # validate_and_update(item, 'notes', 'Updated notes')
24
+ # @return [Array] The updated item.
22
25
  #
23
- # @note The method checks if the field is a time field (start or end) and processes it accordingly.
24
- # @note If the field is not a time field, it directly updates the field with the new value.
25
- # @note The method returns the updated tracking item if the update was successful.
26
+ # @raise [ArgumentError] If the field is invalid or the new value is invalid.
26
27
  def validate_and_update(item, field, new_value)
27
- return if new_value.nil?
28
-
29
- id = item[0]
30
-
31
- if TIME_FIELDS.include?(field)
32
- process_and_update_time_field(item, field, new_value, id)
28
+ case field
29
+ when 'notes'
30
+ item[4] = new_value
31
+ when 'tag'
32
+ item[3] = new_value
33
+ when 'start'
34
+ item[1] = validate_time(item, 'start', new_value)
35
+ when 'end'
36
+ item[2] = validate_time(item, 'end', new_value)
33
37
  else
34
- @db.update_item(id, field, new_value)
38
+ raise ArgumentError, "Invalid field: #{field}"
35
39
  end
36
-
37
- @db.find_item(id)
40
+ item
38
41
  end
39
42
 
40
- private
41
-
42
- # Processes and updates a time field (start or end) of a tracking item.
43
+ # Validates if a given time string is in a valid format.
43
44
  #
44
- # @param item [Array] The tracking item to be updated.
45
- # @param field [String] The time field to be updated.
46
- # @param date_value [String] The new value for the time field.
47
- # @param id [Integer] The ID of the tracking item.
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.
48
49
  #
49
- # @return [void] This method does not return a value; it performs side effects such as updating the time field.
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.
50
53
  #
51
- # @note The method formats the date value and checks if it is valid.
52
- # @note If the date value is valid, it updates the time field with the new value.
53
- # @note If the date value is invalid, it prints an error message.
54
- def process_and_update_time_field(*args)
55
- item, field, date_value, id = args
56
- formatted_date = TimeHelper.format_time_string(date_value)
54
+ # @raise [ArgumentError] If the time string is not in a valid format.
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?
57
58
 
58
- return print_error(date_value) unless formatted_date
59
+ parsed_time_component = parse_time_string(time_str)
59
60
 
60
- new_date = update_time_field(item, field, formatted_date)
61
- new_value_epoch = new_date.to_i
61
+ start_timestamp = item[1]
62
+ end_timestamp = item[2]
62
63
 
63
- if valid_time_value?(item, field, new_value_epoch, id)
64
- @db.update_item(id, field, new_value_epoch)
65
- else
66
- print_error(new_date)
67
- end
68
- end
64
+ new_datetime = determine_and_create_datetime(item, field, start_timestamp, parsed_time_component)
69
65
 
70
- # Prints an error message for an invalid date.
71
- #
72
- # @param message [String] The error message to be printed.
73
- #
74
- # @return [void] This method does not return a value; it performs side effects such as printing an error message.
75
- #
76
- # @example Print an error message for an invalid date
77
- # print_error('Invalid date: 2023-13-32')
78
- def print_error(message)
79
- puts "Invalid date: #{message}".red
80
- end
66
+ new_epoch = new_datetime.to_i
81
67
 
82
- # Updates a time field (start or end) of a tracking item with a formatted date value.
83
- #
84
- # @param item [Array] The tracking item to be updated.
85
- # @param field [String] The time field to be updated.
86
- # @param new_time [String] The new time value.
87
- #
88
- # @return [Time] The updated time value.
89
- #
90
- # @example Update the 'start' field of a tracking item with a formatted date value
91
- # update_time_field(item, 'start', '11:10:00')
92
- def update_time_field(item, field, new_time)
93
- field_index = Timet::Application::FIELD_INDEX[field]
94
- timestamp = item[field_index]
95
- edit_time = Time.at(timestamp || item[1]).to_s.split
96
- edit_time[1] = new_time
97
- DateTime.strptime(edit_time.join(' '), '%Y-%m-%d %H:%M:%S %z').to_time
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
+ )
76
+
77
+ new_epoch
98
78
  end
99
79
 
100
- # Validates if a new time value is valid for a specific time field (start or end).
80
+ # Validates that the new start or end time does not collide with existing entries.
101
81
  #
102
- # @param item [Array] The tracking item to be validated.
103
- # @param field [String] The time field to be validated.
104
- # @param new_value_epoch [Integer] The new time value in epoch format.
105
- # @param id [Integer] The ID of the tracking item.
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.
106
85
  #
107
- # @return [Boolean] Returns true if the new time value is valid, otherwise false.
108
- #
109
- # @example Validate a new 'start' time value
110
- # valid_time_value?(item, 'start', 1633072800, 1)
111
- def valid_time_value?(*args)
112
- item, field, new_value_epoch, id = args
113
- item_start = fetch_item_start(item)
114
- item_end = fetch_item_end(item)
115
- item_before_end = fetch_item_before_end(id, item_start)
116
- item_after_start = fetch_item_after_start(id)
117
-
118
- if field == 'start'
119
- new_value_epoch >= item_before_end && new_value_epoch <= item_end
120
- else
121
- new_value_epoch >= item_start && new_value_epoch <= item_after_start
122
- 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)
91
+
92
+ check_collision_with_previous_item(field, new_epoch, prev_item)
93
+ check_collision_with_next_item(field, new_epoch, next_item)
123
94
  end
124
95
 
125
- # Fetches the start time of a tracking item.
126
- #
127
- # @param item [Array] The tracking item.
128
- #
129
- # @return [Integer] The start time in epoch format.
130
- #
131
- # @example Fetch the start time of a tracking item
132
- # fetch_item_start(item)
133
- def fetch_item_start(item)
134
- 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')})."
135
103
  end
136
104
 
137
- # Fetches the end time of a tracking item.
138
- #
139
- # @param item [Array] The tracking item.
140
- #
141
- # @return [Integer] The end time in epoch format.
142
- #
143
- # @example Fetch the end time of a tracking item
144
- # fetch_item_end(item)
145
- def fetch_item_end(item)
146
- 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
147
118
  end
148
119
 
149
- # Fetches the end time of the tracking item before the current one.
150
- #
151
- # @param id [Integer] The ID of the current tracking item.
152
- # @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.
153
123
  #
154
- # @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.
155
128
  #
156
- # @example Fetch the end time of the previous tracking item
157
- # fetch_item_before_end(1, 1633072800)
158
- def fetch_item_before_end(id, item_start)
159
- @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)
160
134
  end
161
135
 
162
- # Fetches the start time of the tracking item after the current one.
163
- #
164
- # @param id [Integer] The ID of the current tracking item.
165
- #
166
- # @return [Integer] The start time of the next tracking item in epoch format.
167
- #
168
- # @example Fetch the start time of the next tracking item
169
- # fetch_item_after_start(1)
170
- def fetch_item_after_start(id)
171
- @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'
172
158
  end
173
159
  end
174
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.5'
10
- VERSION = '1.5.5'
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.5
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-02-26 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: