timet 0.8.2 → 0.9.1

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,19 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'sqlite3'
4
-
4
+ require_relative 'status_helper'
5
5
  module Timet
6
6
  # Provides database access for managing time tracking data.
7
7
  class Database
8
+ include StatusHelper
9
+
10
+ # The default path to the SQLite database file.
8
11
  DEFAULT_DATABASE_PATH = File.join(Dir.home, '.timet.db')
9
12
 
13
+ # Initializes a new instance of the Database class.
14
+ #
15
+ # @param database_path [String] The path to the SQLite database file. Defaults to DEFAULT_DATABASE_PATH.
16
+ #
17
+ # @return [void] This method does not return a value; it performs side effects such as initializing the database connection and creating the necessary tables.
18
+ #
19
+ # @example Initialize a new Database instance with the default path
20
+ # Database.new
21
+ #
22
+ # @example Initialize a new Database instance with a custom path
23
+ # Database.new('/path/to/custom.db')
24
+ #
25
+ # @note The method creates a new SQLite3 database connection and initializes the necessary tables if they do not already exist.
10
26
  def initialize(database_path = DEFAULT_DATABASE_PATH)
11
27
  @db = SQLite3::Database.new(database_path)
12
28
  create_table
13
29
  add_notes
14
30
  end
15
31
 
16
- # Creates the items table if it doesn't already exist
32
+ # Creates the items table if it doesn't already exist.
33
+ #
34
+ # @return [void] This method does not return a value; it performs side effects such as executing SQL to create the table.
35
+ #
36
+ # @example Create the items table
37
+ # create_table
38
+ #
39
+ # @note The method executes SQL to create the 'items' table with columns for id, start, end, and tag.
17
40
  def create_table
18
41
  execute_sql(<<-SQL)
19
42
  CREATE TABLE IF NOT EXISTS items (
@@ -26,6 +49,13 @@ module Timet
26
49
  end
27
50
 
28
51
  # Adds a new column named "notes" to the "items" table if it doesn't exist.
52
+ #
53
+ # @return [void] This method does not return a value; it performs side effects such as executing SQL to add the column.
54
+ #
55
+ # @example Add the notes column to the items table
56
+ # add_notes
57
+ #
58
+ # @note The method checks if the 'notes' column already exists and adds it if it does not.
29
59
  def add_notes
30
60
  table_name = 'items'
31
61
  new_column_name = 'notes'
@@ -37,65 +67,129 @@ module Timet
37
67
  puts "Column '#{new_column_name}' added to table '#{table_name}'."
38
68
  end
39
69
 
40
- # Inserts a new item into the items table
70
+ # Inserts a new item into the items table.
71
+ #
72
+ # @param start [Integer] The start time of the item.
73
+ # @param tag [String] The tag associated with the item.
74
+ # @param notes [String] The notes associated with the item.
75
+ #
76
+ # @return [void] This method does not return a value; it performs side effects such as executing SQL to insert the item.
77
+ #
78
+ # @example Insert a new item into the items table
79
+ # insert_item(1633072800, 'work', 'Completed task X')
80
+ #
81
+ # @note The method executes SQL to insert a new row into the 'items' table.
41
82
  def insert_item(start, tag, notes)
42
83
  execute_sql('INSERT INTO items (start, tag, notes) VALUES (?, ?, ?)', [start, tag, notes])
43
84
  end
44
85
 
45
- # Updates the end time of the last item
46
- def update(stop)
47
- last_id = fetch_last_id
48
- return unless last_id
86
+ # Updates an existing item in the items table.
87
+ #
88
+ # @param id [Integer] The ID of the item to be updated.
89
+ # @param field [String] The field to be updated.
90
+ # @param value [String, Integer, nil] The new value for the specified field.
91
+ #
92
+ # @return [void] This method does not return a value; it performs side effects such as executing SQL to update the item.
93
+ #
94
+ # @example Update the tag of an item with ID 1
95
+ # update_item(1, 'tag', 'updated_work')
96
+ #
97
+ # @note The method executes SQL to update the specified field of the item with the given ID.
98
+ def update_item(id, field, value)
99
+ return if %w[start end].include?(field) && value.nil?
49
100
 
50
- execute_sql('UPDATE items SET end = ? WHERE id = ?', [stop, last_id])
101
+ execute_sql("UPDATE items SET #{field}='#{value}' WHERE id = #{id}")
51
102
  end
52
103
 
104
+ # Deletes an item from the items table.
105
+ #
106
+ # @param id [Integer] The ID of the item to be deleted.
107
+ #
108
+ # @return [void] This method does not return a value; it performs side effects such as executing SQL to delete the item.
109
+ #
110
+ # @example Delete an item with ID 1
111
+ # delete_item(1)
112
+ #
113
+ # @note The method executes SQL to delete the item with the given ID from the 'items' table.
53
114
  def delete_item(id)
54
115
  execute_sql("DELETE FROM items WHERE id = #{id}")
55
116
  end
56
117
 
57
- def update_item(id, field, value)
58
- return if %w[start end].include?(field) && value.nil?
59
-
60
- execute_sql("UPDATE items SET #{field}='#{value}' WHERE id = #{id}")
61
- end
62
-
63
- # Fetches the ID of the last inserted item
118
+ # Fetches the ID of the last inserted item.
119
+ #
120
+ # @return [Integer, nil] The ID of the last inserted item, or nil if no items exist.
121
+ #
122
+ # @example Fetch the last inserted item ID
123
+ # fetch_last_id
124
+ #
125
+ # @note The method executes SQL to fetch the ID of the last inserted item.
64
126
  def fetch_last_id
65
127
  result = execute_sql('SELECT id FROM items ORDER BY id DESC LIMIT 1').first
66
128
  result ? result[0] : nil
67
129
  end
68
130
 
131
+ # Fetches the last item from the items table.
132
+ #
133
+ # @return [Array, nil] The last item as an array, or nil if no items exist.
134
+ #
135
+ # @example Fetch the last item
136
+ # last_item
137
+ #
138
+ # @note The method executes SQL to fetch the last item from the 'items' table.
69
139
  def last_item
70
140
  execute_sql('SELECT * FROM items ORDER BY id DESC LIMIT 1').first
71
141
  end
72
142
 
143
+ # Determines the status of the last item in the items table.
144
+ #
145
+ # @return [Symbol] The status of the last item. Possible values are :no_items, :in_progress, or :complete.
146
+ #
147
+ # @example Determine the status of the last item
148
+ # last_item_status
149
+ #
150
+ # @note The method executes SQL to fetch the last item and determines its status using the `StatusHelper` module.
73
151
  def last_item_status
74
152
  result = execute_sql('SELECT id, end FROM items ORDER BY id DESC LIMIT 1')
75
153
  StatusHelper.determine_status(result)
76
154
  end
77
155
 
156
+ # Finds an item in the items table by its ID.
157
+ #
158
+ # @param id [Integer] The ID of the item to be found.
159
+ #
160
+ # @return [Array, nil] The item as an array, or nil if the item does not exist.
161
+ #
162
+ # @example Find an item with ID 1
163
+ # find_item(1)
164
+ #
165
+ # @note The method executes SQL to find the item with the given ID in the 'items' table.
78
166
  def find_item(id)
79
167
  execute_sql("select * from items where id=#{id}").first
80
168
  end
81
169
 
82
- # Calculates the total time elapsed since the last recorded time.
83
- def total_time
84
- last_item = execute_sql('SELECT * FROM items ORDER BY id DESC LIMIT 1').first
85
- return '00:00:00' unless last_item
86
-
87
- start_time = last_item[1]
88
- end_time = last_item[2]
89
-
90
- total_seconds = end_time ? end_time - start_time : TimeHelper.current_timestamp - start_time
91
- seconds_to_hms(total_seconds)
92
- end
93
-
170
+ # Fetches all items from the items table that have a start time greater than or equal to today.
171
+ #
172
+ # @return [Array] An array of items.
173
+ #
174
+ # @example Fetch all items from today
175
+ # all_items
176
+ #
177
+ # @note The method executes SQL to fetch all items from the 'items' table that have a start time greater than or equal to today.
94
178
  def all_items
95
179
  execute_sql("SELECT * FROM items where start >= '#{Date.today.to_time.to_i}' ORDER BY id DESC")
96
180
  end
97
181
 
98
- # Executes a SQL query and returns the result
182
+ # Executes a SQL query and returns the result.
183
+ #
184
+ # @param sql [String] The SQL query to execute.
185
+ # @param params [Array] The parameters to bind to the SQL query.
186
+ #
187
+ # @return [Array] The result of the SQL query.
188
+ #
189
+ # @example Execute a SQL query
190
+ # execute_sql('SELECT * FROM items WHERE id = ?', [1])
191
+ #
192
+ # @note The method executes the given SQL query with the provided parameters and returns the result.
99
193
  def execute_sql(sql, params = [])
100
194
  @db.execute(sql, params)
101
195
  rescue SQLite3::SQLException => e
@@ -103,12 +197,28 @@ module Timet
103
197
  []
104
198
  end
105
199
 
106
- # Closes the database connection
200
+ # Closes the database connection.
201
+ #
202
+ # @return [void] This method does not return a value; it performs side effects such as closing the database connection.
203
+ #
204
+ # @example Close the database connection
205
+ # close
206
+ #
207
+ # @note The method closes the SQLite3 database connection.
107
208
  def close
108
209
  @db&.close
109
210
  end
110
211
 
111
212
  # Converts a given number of seconds into a human-readable HH:MM:SS format.
213
+ #
214
+ # @param seconds [Integer] The number of seconds to convert.
215
+ #
216
+ # @return [String] The formatted time in HH:MM:SS format.
217
+ #
218
+ # @example Convert 3661 seconds to HH:MM:SS format
219
+ # seconds_to_hms(3661) # => '01:01:01'
220
+ #
221
+ # @note The method converts the given number of seconds into hours, minutes, and seconds, and formats them as HH:MM:SS.
112
222
  def seconds_to_hms(seconds)
113
223
  hours, remainder = seconds.divmod(3600)
114
224
  minutes, seconds = remainder.divmod(60)
@@ -4,6 +4,14 @@ 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
+ # Formats the header of the time tracking report table.
8
+ #
9
+ # @return [void] This method does not return a value; it performs side effects such as printing the formatted header.
10
+ #
11
+ # @example Format and print the table header
12
+ # format_table_header
13
+ #
14
+ # @note The method constructs a string representing the table header and prints it.
7
15
  def format_table_header
8
16
  header = <<~TABLE
9
17
  Tracked time report \u001b[31m[#{@filter}]\033[0m:
@@ -14,16 +22,42 @@ module Timet
14
22
  puts header
15
23
  end
16
24
 
25
+ # Formats the separator line for the time tracking report table.
26
+ #
27
+ # @return [String] The formatted separator line.
28
+ #
29
+ # @example Get the formatted table separator
30
+ # format_table_separator # => '+-------+------------+--------+----------+----------+----------+--------------------------+'
31
+ #
32
+ # @note The method returns a string representing the separator line for the table.
17
33
  def format_table_separator
18
34
  '+-------+------------+--------+----------+----------+----------+--------------------------+'
19
35
  end
20
36
 
37
+ # Formats a row of the time tracking report table.
38
+ #
39
+ # @param row [Array] The row data to be formatted.
40
+ # @return [String] The formatted row.
41
+ #
42
+ # @example Format a table row
43
+ # format_table_row(1, 'work', '2023-10-01', '12:00:00', '14:00:00', 7200, 'Completed task X')
44
+ #
45
+ # @note The method formats each element of the row and constructs a string representing the formatted row.
21
46
  def format_table_row(*row)
22
47
  id, tag, start_date, start_time, end_time, duration, notes = row
23
48
  "| #{id.to_s.rjust(5)} | #{start_date} | #{tag.ljust(6)} | #{start_time.split[1]} | " \
24
49
  "#{end_time.split[1].rjust(8)} | #{@db.seconds_to_hms(duration).rjust(8)} | #{format_notes(notes)} |"
25
50
  end
26
51
 
52
+ # Formats the notes column of the time tracking report table.
53
+ #
54
+ # @param notes [String, nil] The notes to be formatted.
55
+ # @return [String] The formatted notes.
56
+ #
57
+ # @example Format notes
58
+ # format_notes('This is a long note that needs to be truncated')
59
+ #
60
+ # @note The method truncates the notes to a maximum of 20 characters and pads them to a fixed width.
27
61
  def format_notes(notes)
28
62
  return ' ' * 23 if notes.nil?
29
63
 
@@ -1,8 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Timet
4
- # Determines the status of a time tracking result based on the presence and end time of items.
4
+ # Provides helper methods to determine the status of time tracking results.
5
5
  module StatusHelper
6
+ # Determines the status of a time tracking result based on the presence and end time of items.
7
+ #
8
+ # @param result [Array] The result set containing time tracking items.
9
+ #
10
+ # @return [Symbol] The status of the time tracking result. Possible values are :no_items, :in_progress, or :complete.
11
+ #
12
+ # @example Determine the status of an empty result set
13
+ # StatusHelper.determine_status([]) # => :no_items
14
+ #
15
+ # @example Determine the status of a result set with an in-progress item
16
+ # StatusHelper.determine_status([[1, nil]]) # => :in_progress
17
+ #
18
+ # @example Determine the status of a result set with a completed item
19
+ # StatusHelper.determine_status([[1, 1633072800]]) # => :complete
20
+ #
21
+ # @note The method checks if the result set is empty and returns :no_items if true.
22
+ # @note If the last item in the result set has no end time, it returns :in_progress.
23
+ # @note If the last item in the result set has an end time, it returns :complete.
6
24
  def self.determine_status(result)
7
25
  return :no_items if result.empty?
8
26
 
@@ -7,38 +7,91 @@ module Timet
7
7
  # - calculating the duration between two timestamps
8
8
  # - converting a Date object to a timestamp
9
9
  module TimeHelper
10
+ # Formats a timestamp into a specific format.
11
+ #
12
+ # @param timestamp [Integer] The timestamp to format.
13
+ # @return [String, nil] The formatted time string in 'YYYY-MM-DD HH:MM:SS' format, or nil if the timestamp is nil.
14
+ #
15
+ # @example Format a timestamp
16
+ # TimeHelper.format_time(1633072800) # => '2021-10-01 12:00:00'
10
17
  def self.format_time(timestamp)
11
18
  return nil if timestamp.nil?
12
19
 
13
20
  Time.at(timestamp).strftime('%Y-%m-%d %H:%M:%S')
14
21
  end
15
22
 
23
+ # Converts a timestamp to a date string.
24
+ #
25
+ # @param timestamp [Integer] The timestamp to convert.
26
+ # @return [String, nil] The date string in 'YYYY-MM-DD' format, or nil if the timestamp is nil.
27
+ #
28
+ # @example Convert a timestamp to a date string
29
+ # TimeHelper.timestamp_to_date(1633072800) # => '2021-10-01'
16
30
  def self.timestamp_to_date(timestamp)
17
31
  return nil if timestamp.nil?
18
32
 
19
33
  Time.at(timestamp).strftime('%Y-%m-%d')
20
34
  end
21
35
 
36
+ # Converts a timestamp to a time string.
37
+ #
38
+ # @param timestamp [Integer] The timestamp to convert.
39
+ # @return [String, nil] The time string in 'HH:MM:SS' format, or nil if the timestamp is nil.
40
+ #
41
+ # @example Convert a timestamp to a time string
42
+ # TimeHelper.timestamp_to_time(1633072800) # => '12:00:00'
22
43
  def self.timestamp_to_time(timestamp)
23
44
  return nil if timestamp.nil?
24
45
 
25
46
  Time.at(timestamp).strftime('%H:%M:%S')
26
47
  end
27
48
 
49
+ # Calculates the duration between two timestamps.
50
+ #
51
+ # @param start_time [Integer] The start timestamp.
52
+ # @param end_time [Integer, nil] The end timestamp. If nil, the current timestamp is used.
53
+ # @return [Integer] The duration in seconds.
54
+ #
55
+ # @example Calculate the duration between two timestamps
56
+ # TimeHelper.calculate_duration(1633072800, 1633076400) # => 3600
28
57
  def self.calculate_duration(start_time, end_time)
29
58
  end_time = end_time ? Time.at(end_time) : current_timestamp
30
59
  (end_time - start_time).to_i
31
60
  end
32
61
 
62
+ # Converts a Date object to a timestamp.
63
+ #
64
+ # @param date [Date] The Date object to convert.
65
+ # @return [Integer] The timestamp.
66
+ #
67
+ # @example Convert a Date object to a timestamp
68
+ # TimeHelper.date_to_timestamp(Date.new(2021, 10, 1)) # => 1633072800
33
69
  def self.date_to_timestamp(date)
34
70
  date.to_time.to_i
35
71
  end
36
72
 
73
+ # Calculates the end time based on the start date and end date.
74
+ #
75
+ # @param start_date [Date] The start date.
76
+ # @param end_date [Date, nil] The end date. If nil, the start date + 1 day is used.
77
+ # @return [Integer] The end timestamp.
78
+ #
79
+ # @example Calculate the end time
80
+ # TimeHelper.calculate_end_time(Date.new(2021, 10, 1), Date.new(2021, 10, 2)) # => 1633159200
37
81
  def self.calculate_end_time(start_date, end_date)
38
- end_date ||= start_date + 1
82
+ end_date = end_date ? end_date + 1 : start_date + 1
39
83
  date_to_timestamp(end_date)
40
84
  end
41
85
 
86
+ # Extracts the date from a list of items based on the index.
87
+ #
88
+ # @param items [Array] The list of items.
89
+ # @param idx [Integer] The index of the current item.
90
+ # @return [String, nil] The date string in 'YYYY-MM-DD' format, or nil if the date is the same as the previous item.
91
+ #
92
+ # @example Extract the date from a list of items
93
+ # items = [[1, 1633072800], [2, 1633159200]]
94
+ # TimeHelper.extract_date(items, 1) # => '2021-10-02'
42
95
  def self.extract_date(items, idx)
43
96
  current_start_date = items[idx][1]
44
97
  date = TimeHelper.timestamp_to_date(current_start_date)
@@ -50,17 +103,17 @@ module Timet
50
103
  # Formats a time string into a standard HH:MM:SS format.
51
104
  #
52
105
  # @param input [String] The input string to format.
53
- # @return [String] The formatted time string in HH:MM:SS format, or nil if the input is invalid.
54
- #
55
- # @example
56
- # TimeHelper.format_time_string('123456') # => "12:34:56"
57
- # TimeHelper.format_time_string('1234567') # => "12:34:56"
58
- # TimeHelper.format_time_string('1234') # => "12:34:00"
59
- # TimeHelper.format_time_string('123') # => "12:30:00"
60
- # TimeHelper.format_time_string('12') # => "12:00:00"
61
- # TimeHelper.format_time_string('1') # => "01:00:00"
62
- # TimeHelper.format_time_string('127122') # => nil
63
- # TimeHelper.format_time_string('abc') # => nil
106
+ # @return [String, nil] The formatted time string in HH:MM:SS format, or nil if the input is invalid.
107
+ #
108
+ # @example Format a time string
109
+ # TimeHelper.format_time_string('123456') # => "12:34:56"
110
+ # TimeHelper.format_time_string('1234567') # => "12:34:56"
111
+ # TimeHelper.format_time_string('1234') # => "12:34:00"
112
+ # TimeHelper.format_time_string('123') # => "12:30:00"
113
+ # TimeHelper.format_time_string('12') # => "12:00:00"
114
+ # TimeHelper.format_time_string('1') # => "01:00:00"
115
+ # TimeHelper.format_time_string('127122') # => nil
116
+ # TimeHelper.format_time_string('abc') # => nil
64
117
  def self.format_time_string(input)
65
118
  return nil if input.nil? || input.empty?
66
119
 
@@ -73,6 +126,13 @@ module Timet
73
126
  format('%<hours>02d:%<minutes>02d:%<seconds>02d', hours: hours, minutes: minutes, seconds: seconds)
74
127
  end
75
128
 
129
+ # Parses time components from a string of digits.
130
+ #
131
+ # @param digits [String] The string of digits to parse.
132
+ # @return [Array] An array containing the hours, minutes, and seconds.
133
+ #
134
+ # @example Parse time components
135
+ # TimeHelper.parse_time_components('123456') # => [12, 34, 56]
76
136
  def self.parse_time_components(digits)
77
137
  padded_digits = case digits.size
78
138
  when 1 then "0#{digits}0000"
@@ -85,10 +145,26 @@ module Timet
85
145
  padded_digits.scan(/.{2}/).map(&:to_i)
86
146
  end
87
147
 
148
+ # Validates the time components.
149
+ #
150
+ # @param hours [Integer] The hours component.
151
+ # @param minutes [Integer] The minutes component.
152
+ # @param seconds [Integer] The seconds component.
153
+ # @return [Boolean] True if the time components are valid, otherwise false.
154
+ #
155
+ # @example Validate time components
156
+ # TimeHelper.valid_time?(12, 34, 56) # => true
157
+ # TimeHelper.valid_time?(25, 34, 56) # => false
88
158
  def self.valid_time?(hours, minutes, seconds)
89
159
  hours < 24 && minutes < 60 && seconds < 60
90
160
  end
91
161
 
162
+ # Returns the current timestamp.
163
+ #
164
+ # @return [Integer] The current timestamp.
165
+ #
166
+ # @example Get the current timestamp
167
+ # TimeHelper.current_timestamp
92
168
  def self.current_timestamp
93
169
  Time.now.utc.to_i
94
170
  end