toilet_tracker 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2cd32c62afad4d76b2c46dc4747fd6140c458dd7d5d7ee63d1d4c562761087f2
4
- data.tar.gz: dc38bc38cd7f5610a6fde62c861e9e38d0c46908f2703098f775ffad8f32ae71
3
+ metadata.gz: 64738532b1c80623a41ce64ead3709e4802702b03f87c593ccbc7edbeb1ab7a6
4
+ data.tar.gz: 18c6967a7280153c7e8b0022bb4e8d42e2a422870e3d0af92e5a5b57c23f57eb
5
5
  SHA512:
6
- metadata.gz: 7f347d353402c1b4c3189ac6fabc9098b03c32caf6c04d729e17320e07693778ff81c698540f47923d24f585f189b02bdb913cc940f89dae98cb3d475c312da6
7
- data.tar.gz: 7ff87b8075a25a879a96bcdb6c903e03d09419c45e1285b669a48f9184f6995852be152e1cdf98c46c08933e37515898d21818b29fc24dc5d3c3f4dc066ef1e1
6
+ metadata.gz: 1e45b15dc41c8d141284bd94abb2d650052722b02344830f8b06fcb264d7e2e0cb320ac2c845d11280eb3e1faa1cfa12ff2b670a65df4b5535f7b1b563517723
7
+ data.tar.gz: 86504e632326c8f0c21a6ee3b7845bc129a809e1573c6efc2796232eed6dacffd02037a718c556db233b7e64aee1ab6cbc69efa701b4a657a7429eba492eeb94
data/.rubocop.yml CHANGED
@@ -1,37 +1,2 @@
1
- AllCops:
2
- NewCops: enable
3
- SuggestExtensions: false
4
-
5
- # Disable metrics cops for now
6
- Metrics/BlockLength:
7
- Enabled: false
8
-
9
- Metrics/MethodLength:
10
- Enabled: false
11
-
12
- Metrics/AbcSize:
13
- Enabled: false
14
-
15
- Metrics/CyclomaticComplexity:
16
- Enabled: false
17
-
18
- Metrics/ClassLength:
19
- Enabled: false
20
-
21
- Metrics/PerceivedComplexity:
22
- Enabled: false
23
-
24
- # Disable documentation requirement
25
- Style/Documentation:
26
- Enabled: false
27
-
28
- # Increase line length limit or disable
29
- Layout/LineLength:
30
- Max: 200
31
-
32
- # Disable remaining Lint cops that need manual fixes
33
- Lint/DuplicateBranch:
34
- Enabled: false
35
-
36
- Lint/UnreachableLoop:
37
- Enabled: false
1
+ inherit_gem:
2
+ rubocop-rails-omakase: rubocop.yml
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.1] - 2025-12-18
4
+
5
+ ### Added
6
+ - Fractional timezone shift support (`+5:30` → 5.5 hours)
7
+ - Time-only patterns for events and settings (`💩 14:30`, `🚽 +5:30 14:30`)
8
+ - Timezone-to-shift conversion (returns calculated shift instead of timezone name)
9
+ - New regex patterns with example comments for clarity
10
+
11
+ ### Changed
12
+ - `Configuration#valid_shift?` now accepts Float values
13
+ - `TimezoneService#calculate_shift` returns Float for fractional hours
14
+ - Replaced StandardRB with RuboCop Omakase for linting
15
+
3
16
  ## [0.1.0] - 2025-06-26
4
17
 
5
18
  - Initial release
data/README.md CHANGED
@@ -16,7 +16,7 @@ A modern Ruby gem for parsing WhatsApp messages containing toilet tracking data
16
16
  ## Requirements
17
17
 
18
18
  - **Ruby 3.4+** - Uses modern Ruby features for optimal performance
19
- - **StandardRB** - Zero-configuration linting and formatting
19
+ - **RuboCop Omakase** - Opinionated Ruby styling from the Rails team
20
20
 
21
21
  ## Installation
22
22
 
@@ -186,11 +186,11 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
186
186
  # Run tests
187
187
  bundle exec rspec
188
188
 
189
- # Check code style (StandardRB)
190
- bundle exec standardrb
189
+ # Check code style (RuboCop)
190
+ bundle exec rubocop
191
191
 
192
192
  # Auto-fix style issues
193
- bundle exec standardrb --fix
193
+ bundle exec rubocop -a
194
194
 
195
195
  # Install gem locally
196
196
  bundle exec rake install
@@ -198,7 +198,7 @@ bundle exec rake install
198
198
 
199
199
  ### Code Quality
200
200
 
201
- This project uses **StandardRB** for zero-configuration Ruby linting and formatting. StandardRB eliminates style debates by providing an opinionated, unconfigurable set of rules.
201
+ This project uses **RuboCop Omakase** for opinionated Ruby linting and formatting. Omakase provides the Rails team's preferred styling rules.
202
202
 
203
203
  Key principles:
204
204
  - **Ruby 3.4+ features** - Modern syntax and performance optimizations
data/exe/toilet_tracker CHANGED
@@ -95,7 +95,7 @@ module ToiletTracker
95
95
 
96
96
  when :manual_entry
97
97
  message = prompt.ask('Enter WhatsApp message:')
98
- results = ToiletTracker.parse_lines([message])
98
+ results = ToiletTracker.parse_lines([ message ])
99
99
  handle_interactive_results(results, prompt)
100
100
 
101
101
  when :exit
@@ -103,7 +103,7 @@ module ToiletTracker
103
103
  break
104
104
  end
105
105
 
106
- prompt.keypress('Press any key to continue...', keys: [:return])
106
+ prompt.keypress('Press any key to continue...', keys: [ :return ])
107
107
  end
108
108
  end
109
109
 
@@ -177,7 +177,7 @@ module ToiletTracker
177
177
  when 'csv'
178
178
  require 'csv'
179
179
  CSV.open(filename, 'w') do |csv|
180
- csv << ['Type', 'Status', 'Sender', 'Message Time', 'Event Time', 'Shift', 'Content']
180
+ csv << [ 'Type', 'Status', 'Sender', 'Message Time', 'Event Time', 'Shift', 'Content' ]
181
181
  filter_results(results).each do |result|
182
182
  next unless result
183
183
 
@@ -199,7 +199,7 @@ module ToiletTracker
199
199
  if filtered_results.empty?
200
200
  file.puts 'No results found.'
201
201
  else
202
- headers = ['Type', 'Status', 'Sender', 'Message Time', 'Event Time', 'Shift', 'Content']
202
+ headers = [ 'Type', 'Status', 'Sender', 'Message Time', 'Event Time', 'Shift', 'Content' ]
203
203
  rows = filtered_results.filter_map do |result|
204
204
  next unless result
205
205
 
@@ -216,7 +216,7 @@ module ToiletTracker
216
216
 
217
217
  # Calculate column widths
218
218
  widths = headers.map.with_index do |header, i|
219
- [header.length, *rows.map { |row| row[i].to_s.length }].max
219
+ [ header.length, *rows.map { |row| row[i].to_s.length } ].max
220
220
  end
221
221
 
222
222
  # Write header
@@ -296,7 +296,7 @@ module ToiletTracker
296
296
 
297
297
  def output_csv(results)
298
298
  CSV($stdout) do |csv|
299
- csv << ['Type', 'Status', 'Sender', 'Message Time', 'Event Time', 'Shift', 'Content']
299
+ csv << [ 'Type', 'Status', 'Sender', 'Message Time', 'Event Time', 'Shift', 'Content' ]
300
300
 
301
301
  results.each do |result|
302
302
  next unless result
@@ -318,7 +318,7 @@ module ToiletTracker
318
318
  def output_table(results)
319
319
  return puts 'No results found.' if results.empty?
320
320
 
321
- headers = ['Type', 'Status', 'Sender', 'Message Time', 'Event Time', 'Shift', 'Content']
321
+ headers = [ 'Type', 'Status', 'Sender', 'Message Time', 'Event Time', 'Shift', 'Content' ]
322
322
  rows = results.filter_map do |result|
323
323
  next unless result
324
324
 
@@ -335,7 +335,7 @@ module ToiletTracker
335
335
 
336
336
  # Calculate column widths
337
337
  widths = headers.map.with_index do |header, i|
338
- [header.length, *rows.map { |row| row[i].to_s.length }].max
338
+ [ header.length, *rows.map { |row| row[i].to_s.length } ].max
339
339
  end
340
340
 
341
341
  # Print header
@@ -1,30 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/time'
3
+ require "active_support/time"
4
4
 
5
5
  module ToiletTracker
6
6
  class Configuration
7
7
  attr_accessor :default_timezone, :max_shift_hours, :message_patterns, :date_formats
8
8
 
9
9
  def initialize
10
- @default_timezone = 'UTC'
10
+ @default_timezone = "UTC"
11
11
  @max_shift_hours = 12
12
12
  @message_patterns = {
13
- shift_set: /🚽\s*([+-]?\d+)$/,
14
- shift_set_at_time: /🚽\s*([+-]?\d+)\s+(.+)$/,
13
+ # 💩 14:30
14
+ event_at_time_only: /💩\s*(\d{1,2}:\d{2})$/,
15
+ # 💩 +5 14:30, 💩 +5:30 14:30
16
+ event_at_time_only_shifted: /💩\s*([+-]?\d+(?::\d{2})?)\s+(\d{1,2}:\d{2})$/,
17
+ # 💩 Asia/Kolkata 14:30
18
+ event_in_timezone_at_time_only: %r{💩\s*([A-Za-z_/][A-Za-z0-9_/+-]*)\s+(\d{1,2}:\d{2})$},
19
+ # 🚽 +5 14:30, 🚽 +5:30 14:30
20
+ shift_set_at_time_only: /🚽\s*([+-]?\d+(?::\d{2})?)\s+(\d{1,2}:\d{2})$/,
21
+ # 🚽 Asia/Kolkata 14:30
22
+ timezone_set_at_time_only: %r{🚽\s*([A-Za-z_/][A-Za-z0-9_/+-]*)\s+(\d{1,2}:\d{2})$},
23
+ # 🚽 +2, 🚽 -3, 🚽 +5:30
24
+ shift_set: /🚽\s*([+-]?\d+(?::\d{2})?)$/,
25
+ # 🚽 +2 2025-01-15 14:30, 🚽 +5:30 2025-01-15 14:30
26
+ shift_set_at_time: %r{🚽\s*([+-]?\d+(?::\d{2})?)\s+(\d{4}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}:\d{1,2}(?::\d{1,2})?)$},
27
+ # 🚽 Europe/Rome, 🚽 PST
15
28
  timezone_set: %r{🚽\s*([A-Za-z_/][A-Za-z0-9_/+-]*)$},
29
+ # 🚽 Asia/Kolkata 2025-01-15 14:30
30
+ timezone_set_at_time: %r{🚽\s*([A-Za-z_/][A-Za-z0-9_/+-]*)\s+(\d{4}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}:\d{1,2}(?::\d{1,2})?)$},
31
+ # 💩
16
32
  event_now: /💩$/,
17
- event_now_shifted: /💩\s*([+-]?\d+)$/,
33
+ # 💩 +2, 💩 -1, 💩 +5:30
34
+ event_now_shifted: /💩\s*([+-]?\d+(?::\d{2})?)$/,
35
+ # 💩 2025-01-15 14:30
18
36
  event_at_time: %r{💩\s*(\d{4}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}:\d{1,2}(?::\d{1,2})?)$},
19
- event_at_time_shifted: %r{💩\s*([+-]?\d+)\s+(\d{4}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}:\d{1,2}(?::\d{1,2})?)},
37
+ # 💩 +2 2025-01-15 14:30, 💩 +5:30 2025-01-15 14:30
38
+ event_at_time_shifted: %r{💩\s*([+-]?\d+(?::\d{2})?)\s+(\d{4}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}:\d{1,2}(?::\d{1,2})?)$},
39
+ # 💩 Europe/Rome, 💩 PST
20
40
  event_in_timezone: %r{💩\s*([A-Za-z_/][A-Za-z0-9_/+-]*)\s*$},
41
+ # 💩 Asia/Kolkata 2025-01-15 14:30
21
42
  event_in_timezone_at_time: %r{💩\s*([A-Za-z_/][A-Za-z0-9_/+-]*)\s+(\d{4}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}:\d{1,2}(?::\d{1,2})?)$}
22
43
  }
23
44
  @date_formats = [
24
- '%Y-%m-%d %H:%M:%S',
25
- '%Y-%m-%d %H:%M',
26
- '%Y/%m/%d %H:%M:%S',
27
- '%Y/%m/%d %H:%M'
45
+ "%Y-%m-%d %H:%M:%S",
46
+ "%Y-%m-%d %H:%M",
47
+ "%Y/%m/%d %H:%M:%S",
48
+ "%Y/%m/%d %H:%M",
49
+ "%H:%M"
28
50
  ]
29
51
  end
30
52
 
@@ -34,7 +56,7 @@ module ToiletTracker
34
56
  end
35
57
 
36
58
  def valid_shift?(shift)
37
- shift.is_a?(Integer) && shift.abs <= max_shift_hours
59
+ (shift.is_a?(Integer) || shift.is_a?(Float)) && shift.abs <= max_shift_hours
38
60
  end
39
61
 
40
62
  def whatsapp_message_pattern
@@ -42,7 +64,7 @@ module ToiletTracker
42
64
  end
43
65
 
44
66
  def edited_message_suffix
45
- '‎<This message was edited>'
67
+ "‎<This message was edited>"
46
68
  end
47
69
  end
48
70
 
@@ -1,20 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'time'
4
- require 'active_support/core_ext/time'
3
+ require "time"
4
+ require "active_support/core_ext/time"
5
5
 
6
6
  module ToiletTracker
7
7
  module Core
8
8
  Message = Data.define(:timestamp, :sender, :content) do
9
9
  def initialize(timestamp:, sender:, content:)
10
10
  parsed_timestamp = case timestamp
11
- when Time, ActiveSupport::TimeWithZone
11
+ when Time, ActiveSupport::TimeWithZone
12
12
  timestamp
13
- when String
13
+ when String
14
14
  Time.parse(timestamp)
15
- else
15
+ else
16
16
  raise ArgumentError, "Invalid timestamp type: #{timestamp.class}"
17
- end
17
+ end
18
18
 
19
19
  super(
20
20
  timestamp: parsed_timestamp,
@@ -10,23 +10,29 @@ module ToiletTracker
10
10
  end
11
11
 
12
12
  def parse(input)
13
- raise NotImplementedError, 'Subclasses must implement #parse'
13
+ raise NotImplementedError, "Subclasses must implement #parse"
14
14
  end
15
15
 
16
16
  def valid?(input)
17
- raise NotImplementedError, 'Subclasses must implement #valid?'
17
+ raise NotImplementedError, "Subclasses must implement #valid?"
18
18
  end
19
19
 
20
20
  protected
21
21
 
22
22
  def clean_content(content)
23
- content.to_s.gsub(config.edited_message_suffix, '').strip
23
+ content.to_s.gsub(config.edited_message_suffix, "").strip
24
24
  end
25
25
 
26
26
  def parse_shift(shift_string)
27
27
  return nil if shift_string.nil? || shift_string.empty?
28
28
 
29
- shift = Integer(shift_string)
29
+ # Check if shift contains colon format (e.g., "+5:30", "-3:45")
30
+ shift = if shift_string.match?(/^[+-]?\d+:\d{2}$/)
31
+ parse_fractional_shift(shift_string)
32
+ else
33
+ Integer(shift_string)
34
+ end
35
+
30
36
  raise Core::InvalidShift, shift_string unless config.valid_shift?(shift)
31
37
 
32
38
  shift
@@ -34,26 +40,63 @@ module ToiletTracker
34
40
  raise Core::InvalidShift, shift_string
35
41
  end
36
42
 
37
- def parse_datetime(datetime_string)
43
+ # "+5:30" -> 5.5, "-3:45" -> -3.75, "+5:00" -> 5 (integer)
44
+ def parse_fractional_shift(shift_string)
45
+ match = shift_string.match(/^([+-])?(\d+):(\d{2})$/)
46
+ raise ArgumentError, "Invalid shift format: #{shift_string}" unless match
47
+
48
+ sign = (match[1] == "-") ? -1 : 1
49
+ hours = match[2].to_i
50
+ minutes = match[3].to_i
51
+
52
+ raise ArgumentError, "Invalid minutes: #{minutes}" if minutes > 59
53
+
54
+ decimal = hours + minutes / 60.0
55
+ minutes.zero? ? sign * hours : sign * decimal
56
+ end
57
+
58
+ def parse_datetime(datetime_string, reference_message = nil)
38
59
  return nil if datetime_string.nil? || datetime_string.empty?
39
60
 
61
+ time_only_result = try_parse_time_only(datetime_string, reference_message)
62
+ return time_only_result if time_only_result
63
+
40
64
  config.date_formats.each do |format|
65
+ next if format == "%H:%M"
41
66
  return config.default_timezone_object.strptime(datetime_string, format)
42
67
  rescue ArgumentError
43
68
  next
44
69
  end
45
70
 
46
- # Fallback to timezone-aware parse
47
71
  config.default_timezone_object.parse(datetime_string)
48
72
  rescue ArgumentError
49
73
  raise Core::InvalidDateTime, datetime_string
50
74
  end
51
75
 
76
+ def try_parse_time_only(datetime_string, reference_message)
77
+ return nil unless reference_message
78
+ return nil unless datetime_string.match?(/^\d{1,2}:\d{2}$/)
79
+
80
+ parsed_time = Time.strptime(datetime_string, "%H:%M")
81
+ date_part = reference_message.timestamp.to_date
82
+
83
+ config.default_timezone_object.local(
84
+ date_part.year,
85
+ date_part.month,
86
+ date_part.day,
87
+ parsed_time.hour,
88
+ parsed_time.min,
89
+ 0
90
+ )
91
+ rescue ArgumentError
92
+ nil
93
+ end
94
+
52
95
  def determine_status(event_time, message_time)
53
96
  return :error unless event_time && message_time
54
97
 
55
98
  time_diff = (message_time - event_time).abs
56
- time_diff > 30.days ? :alert : :ok
99
+ (time_diff > 30.days) ? :alert : :ok
57
100
  end
58
101
  end
59
102
  end
@@ -3,17 +3,20 @@
3
3
  module ToiletTracker
4
4
  module Parsers
5
5
  class MessageParser < BaseParser
6
+ attr_accessor :current_message
7
+
6
8
  def parse(message)
7
- return Core::Result.failure(ArgumentError.new('Message required')) unless message.is_a?(Core::Message)
8
- return Core::Result.failure(ArgumentError.new('Invalid message')) unless message.valid?
9
+ return Core::Result.failure(ArgumentError.new("Message required")) unless message.is_a?(Core::Message)
10
+ return Core::Result.failure(ArgumentError.new("Invalid message")) unless message.valid?
9
11
 
12
+ @current_message = message
10
13
  content = message.content.strip
11
14
 
12
- # Try to match patterns
13
15
  matched_result = try_pattern_matching(content, message)
16
+ @current_message = nil
14
17
  return matched_result if matched_result
15
18
 
16
- # Handle unmatched emoji content
19
+ @current_message = nil
17
20
  handle_unmatched_content(content)
18
21
  end
19
22
 
@@ -43,8 +46,8 @@ module ToiletTracker
43
46
  end
44
47
 
45
48
  def handle_unmatched_content(content)
46
- if content.include?('🚽') || content.include?('💩')
47
- error = Core::InvalidMessageFormat.new(content, 'valid emoji pattern')
49
+ if content.include?("🚽") || content.include?("💩")
50
+ error = Core::InvalidMessageFormat.new(content, "valid emoji pattern")
48
51
  return Core::Result.failure(error)
49
52
  end
50
53
 
@@ -53,25 +56,40 @@ module ToiletTracker
53
56
  end
54
57
 
55
58
  def parse_by_type(type, match, message)
56
- case type
57
- when :shift_set, :shift_set_at_time, :timezone_set
58
- parse_toilet_type(type, match, message)
59
+ mapped_type = map_time_only_pattern(type)
60
+
61
+ case mapped_type
62
+ when :shift_set, :shift_set_at_time
63
+ parse_toilet_type(mapped_type, match, message)
64
+ when :timezone_set
65
+ parse_toilet_timezone(match, message)
66
+ when :timezone_set_at_time
67
+ parse_toilet_timezone_with_time(match, message)
59
68
  when :event_now, :event_now_shifted, :event_at_time, :event_at_time_shifted,
60
69
  :event_in_timezone, :event_in_timezone_at_time
61
- parse_poop_type(type, match, message)
70
+ parse_poop_type(mapped_type, match, message)
62
71
  else
63
72
  raise Core::ParseError, "Unknown message type: #{type}"
64
73
  end
65
74
  end
66
75
 
76
+ def map_time_only_pattern(type)
77
+ case type
78
+ when :event_at_time_only then :event_at_time
79
+ when :event_at_time_only_shifted then :event_at_time_shifted
80
+ when :event_in_timezone_at_time_only then :event_in_timezone_at_time
81
+ when :shift_set_at_time_only then :shift_set_at_time
82
+ when :timezone_set_at_time_only then :timezone_set_at_time
83
+ else type
84
+ end
85
+ end
86
+
67
87
  def parse_toilet_type(type, match, message)
68
88
  case type
69
89
  when :shift_set
70
90
  parse_toilet_shift(match, message)
71
91
  when :shift_set_at_time
72
92
  parse_toilet_shift_with_time(match, message)
73
- when :timezone_set
74
- parse_toilet_timezone(match, message)
75
93
  end
76
94
  end
77
95
 
@@ -106,7 +124,7 @@ module ToiletTracker
106
124
 
107
125
  def parse_toilet_shift_with_time(match, message)
108
126
  shift = parse_shift(match[1])
109
- effective_time = parse_datetime(match[2])
127
+ effective_time = parse_datetime(match[2], @current_message)
110
128
 
111
129
  status = determine_status(effective_time, message.timestamp)
112
130
 
@@ -121,15 +139,43 @@ module ToiletTracker
121
139
 
122
140
  def parse_toilet_timezone(match, message)
123
141
  timezone_str = match[1]
142
+ shift = calculate_shift_from_timezone(timezone_str)
124
143
 
125
- Core::TimezoneResult.new(
144
+ Core::ToiletShiftResult.new(
126
145
  type: :timezone_set,
127
146
  status: :ok,
128
147
  message: message,
129
- timezone: timezone_str
148
+ shift: shift,
149
+ effective_time: message.timestamp
130
150
  )
131
151
  end
132
152
 
153
+ def parse_toilet_timezone_with_time(match, message)
154
+ timezone_str = match[1]
155
+ datetime_str = match[2]
156
+
157
+ effective_time = parse_datetime(datetime_str, @current_message)
158
+ status = determine_status(effective_time, message.timestamp)
159
+ shift = calculate_shift_from_timezone(timezone_str)
160
+
161
+ Core::ToiletShiftResult.new(
162
+ type: :timezone_set_at_time,
163
+ status: status,
164
+ message: message,
165
+ shift: shift,
166
+ effective_time: effective_time
167
+ )
168
+ end
169
+
170
+ def calculate_shift_from_timezone(timezone_str)
171
+ tz_object = ActiveSupport::TimeZone[timezone_str]
172
+ raise Core::InvalidTimezone, timezone_str unless tz_object
173
+
174
+ default_tz = config.default_timezone_object
175
+ shift = (tz_object.utc_offset - default_tz.utc_offset) / 3600.0
176
+ (shift == shift.to_i) ? shift.to_i : shift
177
+ end
178
+
133
179
  def parse_poop_live(message)
134
180
  Core::PoopEventResult.new(
135
181
  type: :event_now,
@@ -153,7 +199,7 @@ module ToiletTracker
153
199
  end
154
200
 
155
201
  def parse_poop_past(match, message)
156
- poop_time = parse_datetime(match[1])
202
+ poop_time = parse_datetime(match[1], @current_message)
157
203
  status = determine_status(poop_time, message.timestamp)
158
204
 
159
205
  Core::PoopEventResult.new(
@@ -167,7 +213,7 @@ module ToiletTracker
167
213
 
168
214
  def parse_poop_past_with_shift(match, message)
169
215
  shift = parse_shift(match[1])
170
- poop_time = parse_datetime(match[2])
216
+ poop_time = parse_datetime(match[2], @current_message)
171
217
  status = determine_status(poop_time, message.timestamp)
172
218
 
173
219
  Core::PoopEventResult.new(
@@ -193,7 +239,7 @@ module ToiletTracker
193
239
 
194
240
  def parse_poop_timezone_with_time(match, message)
195
241
  timezone_str = match[1]
196
- poop_time = parse_datetime(match[2])
242
+ poop_time = parse_datetime(match[2], @current_message)
197
243
  status = determine_status(poop_time, message.timestamp)
198
244
 
199
245
  Core::PoopEventResult.new(
@@ -24,11 +24,11 @@ module ToiletTracker
24
24
  private
25
25
 
26
26
  def failure_for_invalid_format(line)
27
- Core::Result.failure(Core::InvalidMessageFormat.new(line, 'WhatsApp format'))
27
+ Core::Result.failure(Core::InvalidMessageFormat.new(line, "WhatsApp format"))
28
28
  end
29
29
 
30
30
  def failure_for_invalid_sender(line)
31
- Core::Result.failure(Core::InvalidMessageFormat.new(line, 'Empty sender'))
31
+ Core::Result.failure(Core::InvalidMessageFormat.new(line, "Empty sender"))
32
32
  end
33
33
 
34
34
  def build_message_from_match(match)
@@ -46,16 +46,16 @@ module ToiletTracker
46
46
  datetime_string = "#{day}/#{month}/#{full_year} #{time}"
47
47
 
48
48
  # Parse in the default timezone
49
- config.default_timezone_object.strptime(datetime_string, '%d/%m/%Y %H:%M:%S')
49
+ config.default_timezone_object.strptime(datetime_string, "%d/%m/%Y %H:%M:%S")
50
50
  rescue ArgumentError => e
51
51
  raise Core::InvalidDateTime, "#{day}/#{month}/#{year} #{time}: #{e.message}"
52
52
  end
53
53
 
54
54
  def clean_content(content)
55
- return '' if content.nil?
55
+ return "" if content.nil?
56
56
 
57
57
  # Remove the "This message was edited" suffix
58
- content.gsub(config.edited_message_suffix, '').strip
58
+ content.gsub(config.edited_message_suffix, "").strip
59
59
  end
60
60
  end
61
61
  end
@@ -9,7 +9,7 @@ module ToiletTracker
9
9
 
10
10
  def initialize(timezone_service = TimezoneService.new)
11
11
  @timezone_service = timezone_service
12
- @user_shifts = Hash.new { |h, k| h[k] = [default_shift_entry] }
12
+ @user_shifts = Hash.new { |h, k| h[k] = [ default_shift_entry ] }
13
13
  end
14
14
 
15
15
  def set_shift(user, shift, effective_time = Time.current, timezone = nil)
@@ -65,7 +65,7 @@ module ToiletTracker
65
65
  end
66
66
 
67
67
  def clear_history(user)
68
- @user_shifts[user] = [default_shift_entry]
68
+ @user_shifts[user] = [ default_shift_entry ]
69
69
  end
70
70
 
71
71
  def clear_all_history
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/time'
3
+ require "active_support/time"
4
4
 
5
5
  module ToiletTracker
6
6
  module Services
@@ -15,11 +15,11 @@ module ToiletTracker
15
15
  from_zone = resolve_timezone(from_timezone)
16
16
  to_zone = resolve_timezone(to_timezone)
17
17
 
18
- # Calculate offset difference in hours
19
18
  from_offset = from_zone.at(at_time).utc_offset
20
19
  to_offset = to_zone.at(at_time).utc_offset
21
20
 
22
- (to_offset - from_offset) / 3600
21
+ shift = (to_offset - from_offset) / 3600.0
22
+ (shift == shift.to_i) ? shift.to_i : shift
23
23
  end
24
24
 
25
25
  def apply_shift(time, shift_hours)
@@ -82,7 +82,7 @@ module ToiletTracker
82
82
  end
83
83
 
84
84
  def sample_times
85
- [Time.current, Time.current + 6.months]
85
+ [ Time.current, Time.current + 6.months ]
86
86
  end
87
87
 
88
88
  def add_zone_to_map(map, abbr, zone)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'zip'
4
- require 'tempfile'
3
+ require "zip"
4
+ require "tempfile"
5
5
 
6
6
  module ToiletTracker
7
7
  module Utils
@@ -15,7 +15,7 @@ module ToiletTracker
15
15
  end
16
16
 
17
17
  def self.zip_file?(file_path)
18
- File.extname(file_path).downcase == '.zip'
18
+ File.extname(file_path).downcase == ".zip"
19
19
  end
20
20
 
21
21
  def initialize(zip_path)
@@ -26,14 +26,14 @@ module ToiletTracker
26
26
  def extract_chat_file
27
27
  chat_entries = find_chat_entries
28
28
 
29
- raise ChatFileNotFoundError, 'No _chat.txt file found in zip archive' if chat_entries.empty?
29
+ raise ChatFileNotFoundError, "No _chat.txt file found in zip archive" if chat_entries.empty?
30
30
 
31
31
  chat_entry = if chat_entries.size > 1
32
32
  # If multiple _chat.txt files, prefer the one in root or with shortest path
33
- chat_entries.min_by { |entry| entry.name.count('/') }
34
- else
33
+ chat_entries.min_by { |entry| entry.name.count("/") }
34
+ else
35
35
  chat_entries.first
36
- end
36
+ end
37
37
 
38
38
  extract_to_tempfile(chat_entry)
39
39
  end
@@ -64,7 +64,7 @@ module ToiletTracker
64
64
  end
65
65
 
66
66
  def extract_to_tempfile(entry)
67
- temp_file = Tempfile.new(['chat_export', '.txt'])
67
+ temp_file = Tempfile.new([ "chat_export", ".txt" ])
68
68
 
69
69
  begin
70
70
  Zip::File.open(@zip_path) do |_zip|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ToiletTracker
4
- VERSION = '0.1.0'
4
+ VERSION = "0.1.1"
5
5
  end
@@ -1,20 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/time'
3
+ require "active_support/time"
4
4
 
5
- require_relative 'toilet_tracker/version'
6
- require_relative 'toilet_tracker/configuration'
7
- require_relative 'toilet_tracker/core/errors'
8
- require_relative 'toilet_tracker/core/message'
9
- require_relative 'toilet_tracker/core/result'
10
- require_relative 'toilet_tracker/core/parse_result'
11
- require_relative 'toilet_tracker/parsers/base_parser'
12
- require_relative 'toilet_tracker/parsers/whatsapp_parser'
13
- require_relative 'toilet_tracker/parsers/message_parser'
14
- require_relative 'toilet_tracker/services/timezone_service'
15
- require_relative 'toilet_tracker/services/shift_service'
16
- require_relative 'toilet_tracker/services/analysis_service'
17
- require_relative 'toilet_tracker/utils/zip_handler'
5
+ require_relative "toilet_tracker/version"
6
+ require_relative "toilet_tracker/configuration"
7
+ require_relative "toilet_tracker/core/errors"
8
+ require_relative "toilet_tracker/core/message"
9
+ require_relative "toilet_tracker/core/result"
10
+ require_relative "toilet_tracker/core/parse_result"
11
+ require_relative "toilet_tracker/parsers/base_parser"
12
+ require_relative "toilet_tracker/parsers/whatsapp_parser"
13
+ require_relative "toilet_tracker/parsers/message_parser"
14
+ require_relative "toilet_tracker/services/timezone_service"
15
+ require_relative "toilet_tracker/services/shift_service"
16
+ require_relative "toilet_tracker/services/analysis_service"
17
+ require_relative "toilet_tracker/utils/zip_handler"
18
18
 
19
19
  module ToiletTracker
20
20
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toilet_tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - NetherSoul
@@ -106,7 +106,6 @@ files:
106
106
  - ".overcommit.yml"
107
107
  - ".rspec"
108
108
  - ".rubocop.yml"
109
- - ".standard.yml"
110
109
  - CHANGELOG.md
111
110
  - CODE_OF_CONDUCT.md
112
111
  - LICENSE.txt
data/.standard.yml DELETED
@@ -1,2 +0,0 @@
1
- ruby_version: 3.4
2
- fix: true