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.
@@ -6,81 +6,6 @@ module Timet
6
6
  # and validating date formats.
7
7
  # This module is designed to be included in classes that require time report processing functionalities.
8
8
  module TimeReportHelper
9
- # Provides predefined date ranges for filtering.
10
- #
11
- # @return [Hash] A hash containing predefined date ranges.
12
- #
13
- # @example Get the predefined date ranges
14
- # date_ranges
15
- #
16
- # @note The method returns a hash with predefined date ranges for 'today', 'yesterday', 'week', and 'month'.
17
- def date_ranges
18
- today = Date.today
19
- tomorrow = today + 1
20
- {
21
- 'today' => [today, nil],
22
- 'yesterday' => [today - 1, nil],
23
- 'week' => [today - 7, tomorrow],
24
- 'month' => [today - 30, tomorrow]
25
- }
26
- end
27
-
28
- # Formats an item for CSV export.
29
- #
30
- # @param item [Array] The item to format.
31
- #
32
- # @return [Array] The formatted item.
33
- #
34
- # @example Format an item for CSV export
35
- # format_item(item)
36
- #
37
- # @note The method formats the item's ID, start time, end time, tag, and notes.
38
- def format_item(item)
39
- id, start_time, end_time, tags, notes = item
40
- [
41
- id,
42
- TimeHelper.format_time(start_time),
43
- TimeHelper.format_time(end_time),
44
- tags,
45
- notes
46
- ]
47
- end
48
-
49
- # Validates the date format.
50
- #
51
- # @param date_string [String] The date string to validate.
52
- #
53
- # @return [Boolean] True if the date format is valid, otherwise false.
54
- #
55
- # @example Validate the date format
56
- # valid_date_format?('2021-10-01') # => true
57
- #
58
- # @note The method validates the date format for single dates and date ranges.
59
- def valid_date_format?(date_string)
60
- date_format_single = /^\d{4}-\d{2}-\d{2}$/
61
- date_format_range = /^\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}$/
62
-
63
- date_string.match?(date_format_single) || date_string.match?(date_format_range)
64
- end
65
-
66
- # Merges two hashes, summing the numeric values of corresponding keys.
67
- #
68
- # @param base_hash [Hash] The base hash to which the additional hash will be merged.
69
- # @param additional_hash [Hash] The additional hash whose values will be added to the base hash.
70
- # @return [Hash] A new hash with the summed values.
71
- #
72
- # @example
73
- # base_hash = { 'key1' => [10, 'tag1'], 'key2' => [20, 'tag2'] }
74
- # additional_hash = { 'key1' => [5, 'tag1'], 'key3' => [15, 'tag3'] }
75
- # add_hashes(base_hash, additional_hash)
76
- # #=> { 'key1' => [15, 'tag1'], 'key2' => [20, 'tag2'], 'key3' => [15, 'tag3'] }
77
- def add_hashes(base_hash, additional_hash)
78
- base_hash.merge(additional_hash) do |_key, old_value, new_value|
79
- summed_number = old_value[0] + new_value[0]
80
- [summed_number, old_value[1]]
81
- end
82
- end
83
-
84
9
  # Exports the report to a CSV file.
85
10
  #
86
11
  # @return [void] This method does not return a value; it performs side effects such as writing the CSV file.
@@ -101,51 +26,11 @@ module Timet
101
26
  # @return [void]
102
27
  def export_icalendar
103
28
  file_name = "#{ics_filename}.ics"
104
- cal = add_events
29
+ cal = Timet::Utils.add_events(items)
105
30
 
106
31
  File.write(file_name, cal.to_ical)
107
32
 
108
33
  puts "The #{file_name} has been generated."
109
34
  end
110
-
111
- private
112
-
113
- # Creates an iCalendar object and adds events to it.
114
- #
115
- # @return [Icalendar::Calendar] the populated iCalendar object
116
- def add_events
117
- cal = Icalendar::Calendar.new
118
- items.each do |item|
119
- event = create_event(item)
120
- cal.add_event(event)
121
- end
122
- cal.publish
123
- cal
124
- end
125
-
126
- # Creates an iCalendar event from the given item.
127
- #
128
- # @param item [Array] the item containing event details
129
- # @return [Icalendar::Event] the created event
130
- def create_event(item)
131
- dtstart = convert_to_datetime(item[1])
132
- dtend = convert_to_datetime(item[2] || TimeHelper.current_timestamp)
133
-
134
- Icalendar::Event.new.tap do |e|
135
- e.dtstart = dtstart
136
- e.dtend = dtend
137
- e.summary = item[3]
138
- e.description = item[4]
139
- e.ip_class = 'PRIVATE'
140
- end
141
- end
142
-
143
- # Converts a timestamp to a DateTime object.
144
- #
145
- # @param timestamp [Integer] the timestamp to convert
146
- # @return [DateTime] the converted DateTime object
147
- def convert_to_datetime(timestamp)
148
- Time.at(timestamp).to_datetime
149
- end
150
35
  end
151
36
  end
@@ -0,0 +1,93 @@
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 = 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
+ # Updates a time field (start or end) of a tracking item with a formatted date value.
51
+ #
52
+ # @param item [Array] The tracking item to be updated.
53
+ # @param field [String] The time field to be updated.
54
+ # @param new_time [String] The new time value.
55
+ #
56
+ # @return [Time] The updated time value.
57
+ #
58
+ # @example Update the 'start' field of a tracking item with a formatted date value
59
+ # update_time_field(item, 'start', '11:10:00')
60
+ def update_time_field(item, field, new_time)
61
+ field_index = Timet::Application::FIELD_INDEX[field]
62
+ timestamp = item[field_index]
63
+ edit_time = Time.at(timestamp || item[1]).to_s.split
64
+ edit_time[1] = new_time
65
+ DateTime.strptime(edit_time.join(' '), '%Y-%m-%d %H:%M:%S %z').to_time
66
+ end
67
+
68
+ # Validates if a new time value is valid for a specific time field (start or end).
69
+ #
70
+ # @param item [Array] The tracking item to be validated.
71
+ # @param field [String] The time field to be validated.
72
+ # @param new_value_epoch [Integer] The new time value in epoch format.
73
+ # @param id [Integer] The ID of the tracking item.
74
+ #
75
+ # @return [Boolean] Returns true if the new time value is valid, otherwise false.
76
+ #
77
+ # @example Validate a new 'start' time value
78
+ # valid_time_value?(item, 'start', 1633072800, 1)
79
+ def valid_time_value?(*args)
80
+ item, field, new_value_epoch, id = args
81
+ item_start = ItemDataHelper.fetch_item_start(item)
82
+ item_end = ItemDataHelper.fetch_item_end(item)
83
+ item_before_end = ItemDataHelper.fetch_item_before_end(@db, id, item_start)
84
+ item_after_start = ItemDataHelper.fetch_item_after_start(@db, id)
85
+
86
+ if field == 'start'
87
+ new_value_epoch.between?(item_before_end, item_end)
88
+ else
89
+ new_value_epoch.between?(item_start, item_after_start)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Timet
6
+ # Helper module for time validation logic.
7
+ module TimeValidationHelper
8
+ # Parses the time string and raises an ArgumentError if the format is invalid.
9
+ #
10
+ # @param time_str [String] The time string to parse.
11
+ #
12
+ # @return [Time] The parsed time component.
13
+ #
14
+ # @raise [ArgumentError] If the time string is not in a valid format.
15
+ def parse_time_string(time_str)
16
+ Time.parse(time_str)
17
+ rescue ArgumentError
18
+ raise ArgumentError, "Invalid time format: #{time_str}"
19
+ end
20
+
21
+ # Adjusts the end datetime if it's earlier than or same as the start time, assuming it's for the next day.
22
+ #
23
+ # @param field [String] The field being validated ('start' or 'end').
24
+ # @param start_timestamp [Integer, nil] The start timestamp of the item.
25
+ # @param new_datetime [Time] The new datetime object.
26
+ #
27
+ # @return [Time] The adjusted datetime object.
28
+ def adjust_end_datetime(field, start_timestamp, new_datetime)
29
+ # If setting 'end' time and the parsed new_datetime (based on start_date)
30
+ # is earlier than or same as start_time, assume it's for the next calendar day.
31
+ if field == 'end' && start_timestamp && (new_datetime.to_i <= start_timestamp)
32
+ new_datetime += (24 * 60 * 60) # Add one day
33
+ end
34
+ new_datetime
35
+ end
36
+
37
+ # Determines the base date and time for creating a new datetime object.
38
+ #
39
+ # @param item [Array] The item being modified.
40
+ # @param field [String] The field being validated ('start' or 'end').
41
+ # @param start_timestamp [Integer, nil] The start timestamp of the item.
42
+ #
43
+ # @return [Time] The base date and time.
44
+ #
45
+ # @raise [ArgumentError] If the field is 'end' and the start timestamp is not set or if the field is invalid.
46
+ def determine_base_date_time(_item, field, start_timestamp)
47
+ case field
48
+ when 'start'
49
+ determine_start_base_date_time(start_timestamp)
50
+ when 'end'
51
+ determine_end_base_date_time(start_timestamp)
52
+ else
53
+ raise ArgumentError, "Invalid field: #{field}"
54
+ end
55
+ end
56
+
57
+ # Determines the base date and time for the 'start' field.
58
+ #
59
+ # @param start_timestamp [Integer, nil] The start timestamp of the item.
60
+ #
61
+ # @return [Time] The base date and time.
62
+ def determine_start_base_date_time(start_timestamp)
63
+ start_timestamp ? Time.at(start_timestamp) : Time.now
64
+ end
65
+ private :determine_start_base_date_time
66
+
67
+ # Determines the base date and time for the 'end' field.
68
+ #
69
+ # @param start_timestamp [Integer, nil] The start timestamp of the item.
70
+ #
71
+ # @return [Time] The base date and time.
72
+ #
73
+ # @raise [ArgumentError] If the start timestamp is not set.
74
+ def determine_end_base_date_time(start_timestamp)
75
+ # This ensures that start_timestamp is not nil when setting/editing an end time.
76
+ raise ArgumentError, "Cannot set 'end' time because 'start' time is not set." unless start_timestamp
77
+
78
+ Time.at(start_timestamp)
79
+ end
80
+ private :determine_end_base_date_time
81
+
82
+ # Creates a new datetime object based on the parsed time component.
83
+ #
84
+ # @param _base_date_time [Time] The base date and time (not used for date components).
85
+ # @param parsed_time_component [Time] The parsed time component.
86
+ #
87
+ # @return [Time] The new datetime object.
88
+ def create_new_datetime(_base_date_time, parsed_time_component)
89
+ Time.new(
90
+ parsed_time_component.year,
91
+ parsed_time_component.month,
92
+ parsed_time_component.day,
93
+ parsed_time_component.hour,
94
+ parsed_time_component.min,
95
+ parsed_time_component.sec,
96
+ parsed_time_component.utc_offset # Preserve timezone context
97
+ )
98
+ end
99
+
100
+ # Validates that the new datetime is not in the future.
101
+ #
102
+ # @param new_datetime [Time] The new datetime object.
103
+ #
104
+ # @raise [ArgumentError] If the new datetime is in the future.
105
+ def validate_future_date(new_datetime)
106
+ return unless new_datetime > Time.now
107
+
108
+ raise ArgumentError, "Cannot set time to a future date: #{new_datetime.strftime('%Y-%m-%d %H:%M:%S')}"
109
+ end
110
+
111
+ # Validates that the difference between two timestamps is less than 24 hours.
112
+ #
113
+ # @param timestamp1 [Integer] The first timestamp.
114
+ # @param timestamp2 [Integer] The second timestamp.
115
+ #
116
+ # @raise [ArgumentError] If the difference is >= 24 hours.
117
+ def validate_time_difference(timestamp1, timestamp2)
118
+ return unless (timestamp2 - timestamp1).abs >= 24 * 60 * 60
119
+
120
+ raise ArgumentError, 'The difference between start and end time must be less than 24 hours.'
121
+ end
122
+ private :validate_time_difference
123
+
124
+ # Validates the time order (start before end, end after start).
125
+ #
126
+ # @param new_epoch [Integer] The new time in epoch format.
127
+ # @param reference_timestamp [Integer] The reference timestamp (start or end).
128
+ # @param new_datetime [Time] The new datetime object.
129
+ # @param field [String] The field being validated ('start' or 'end').
130
+ #
131
+ # @raise [ArgumentError] If the time order is invalid.
132
+ def validate_time_order(new_epoch, reference_timestamp, new_datetime, field)
133
+ case field
134
+ when 'end'
135
+ if new_epoch <= reference_timestamp
136
+ raise ArgumentError,
137
+ "End time (#{new_datetime.strftime('%Y-%m-%d %H:%M:%S')}) must be after start time " \
138
+ "(#{Time.at(reference_timestamp).strftime('%Y-%m-%d %H:%M:%S')})."
139
+ end
140
+ when 'start'
141
+ if new_epoch >= reference_timestamp
142
+ raise ArgumentError,
143
+ "Start time (#{new_datetime.strftime('%Y-%m-%d %H:%M:%S')}) must be before end time " \
144
+ "(#{Time.at(reference_timestamp).strftime('%Y-%m-%d %H:%M:%S')})."
145
+ end
146
+ end
147
+ end
148
+ private :validate_time_order
149
+
150
+ # Validates the end time against the start time.
151
+ #
152
+ # @param new_epoch [Integer] The new end time in epoch format.
153
+ # @param start_timestamp [Integer] The start timestamp of the item.
154
+ # @param new_datetime [Time] The new datetime object.
155
+ #
156
+ # @raise [ArgumentError] If the end time is not after the start time or the difference is >= 24 hours.
157
+ def validate_end_time(new_epoch, start_timestamp, new_datetime)
158
+ validate_time_order(new_epoch, start_timestamp, new_datetime, 'end')
159
+ validate_time_difference(start_timestamp, new_epoch)
160
+ end
161
+
162
+ # Validates the start time against the end time.
163
+ #
164
+ # @param new_epoch [Integer] The new start time in epoch format.
165
+ # @param end_timestamp [Integer] The end timestamp of the item.
166
+ # @param new_datetime [Time] The new datetime object.
167
+ #
168
+ # @raise [ArgumentError] If the start time is not before the end time or the difference is >= 24 hours.
169
+ def validate_start_time(new_epoch, end_timestamp, new_datetime)
170
+ validate_time_order(new_epoch, end_timestamp, new_datetime, 'start')
171
+ validate_time_difference(new_epoch, end_timestamp)
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'time_helper'
4
+
5
+ module Timet
6
+ # The Utils module provides a collection of general utility methods used across the Timet gem.
7
+ module Utils
8
+ # Merges two hashes, summing the numeric values of corresponding keys.
9
+ #
10
+ # @param base_hash [Hash] The base hash to which the additional hash will be merged.
11
+ # @param additional_hash [Hash] The additional hash whose values will be added to the base hash.
12
+ # @return [Hash] A new hash with the summed values.
13
+ #
14
+ # @example
15
+ # base_hash = { 'key1' => [10, 'tag1'], 'key2' => [20, 'tag2'] }
16
+ # additional_hash = { 'key1' => [5, 'tag1'], 'key3' => [15, 'tag3'] }
17
+ # Utils.add_hashes(base_hash, additional_hash)
18
+ # #=> { 'key1' => [15, 'tag1'], 'key2' => [20, 'tag2'], 'key3' => [15, 'tag3'] }
19
+ def self.add_hashes(base_hash, additional_hash)
20
+ base_hash.merge(additional_hash) do |_key, old_value, new_value|
21
+ summed_number = old_value[0] + new_value[0]
22
+ [summed_number, old_value[1]]
23
+ end
24
+ end
25
+
26
+ # Converts a timestamp to a DateTime object.
27
+ #
28
+ # @param timestamp [Integer] the timestamp to convert
29
+ # @return [DateTime] the converted DateTime object
30
+ def self.convert_to_datetime(timestamp)
31
+ Time.at(timestamp).to_datetime
32
+ end
33
+
34
+ # Provides predefined date ranges for filtering.
35
+ #
36
+ # @return [Hash] A hash containing predefined date ranges.
37
+ #
38
+ # @example Get the predefined date ranges
39
+ # Utils.date_ranges
40
+ #
41
+ # @note The method returns a hash with predefined date ranges for 'today', 'yesterday', 'week', and 'month'.
42
+ def self.date_ranges
43
+ today = Date.today
44
+ tomorrow = today + 1
45
+ {
46
+ 'today' => [today, nil],
47
+ 'yesterday' => [today - 1, nil],
48
+ 'week' => [today - 7, tomorrow],
49
+ 'month' => [today - 30, tomorrow]
50
+ }
51
+ end
52
+
53
+ # Formats an item for CSV export.
54
+ #
55
+ # @param item [Array] The item to format.
56
+ #
57
+ # @return [Array] The formatted item.
58
+ #
59
+ # @example Format an item for CSV export
60
+ # Utils.format_item(item)
61
+ #
62
+ # @note The method formats the item's ID, start time, end time, tag, and notes.
63
+ def self.format_item(item)
64
+ id, start_time, end_time, tags, notes = item
65
+ [
66
+ id,
67
+ TimeHelper.format_time(start_time),
68
+ TimeHelper.format_time(end_time),
69
+ tags,
70
+ notes
71
+ ]
72
+ end
73
+
74
+ # Validates the date format.
75
+ #
76
+ # @param date_string [String] The date string to validate.
77
+ #
78
+ # @return [Boolean] True if the date format is valid, otherwise false.
79
+ #
80
+ # @example Validate the date format
81
+ # Utils.valid_date_format?('2021-10-01') # => true
82
+ #
83
+ # @note The method validates the date format for single dates and date ranges.
84
+ def self.valid_date_format?(date_string)
85
+ date_format_single = /^\d{4}-\d{2}-\d{2}$/
86
+ date_format_range = /^\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}$/
87
+
88
+ date_string.match?(date_format_single) || date_string.match?(date_format_range)
89
+ end
90
+
91
+ # Assigns attributes to an iCalendar event.
92
+ #
93
+ # @param event [Icalendar::Event] the event object
94
+ # @param item [Array] the item containing event details
95
+ def self.assign_event_attributes(event, item)
96
+ dtstart = convert_to_datetime(item[1])
97
+ dtend = convert_to_datetime(item[2] || TimeHelper.current_timestamp)
98
+
99
+ event.dtstart = dtstart
100
+ event.dtend = dtend
101
+ event.summary = item[3]
102
+ event.description = item[4]
103
+ event.ip_class = 'PRIVATE'
104
+ end
105
+
106
+ # Creates an iCalendar event from the given item.
107
+ #
108
+ # @param item [Array] the item containing event details
109
+ # @return [Icalendar::Event] the created event
110
+ def self.create_event(item)
111
+ event = Icalendar::Event.new
112
+ assign_event_attributes(event, item)
113
+ event
114
+ end
115
+
116
+ # Creates an iCalendar object and adds events to it.
117
+ #
118
+ # @param items [Array] the items containing event details
119
+ # @return [Icalendar::Calendar] the populated iCalendar object
120
+ def self.add_events(items)
121
+ cal = Icalendar::Calendar.new
122
+ items.each do |item|
123
+ event = create_event(item)
124
+ cal.add_event(event)
125
+ end
126
+ cal.publish
127
+ cal
128
+ end
129
+ end
130
+ end