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 +4 -4
- data/.rubocop.yml +2 -37
- data/CHANGELOG.md +13 -0
- data/README.md +5 -5
- data/exe/toilet_tracker +8 -8
- data/lib/toilet_tracker/configuration.rb +34 -12
- data/lib/toilet_tracker/core/message.rb +6 -6
- data/lib/toilet_tracker/parsers/base_parser.rb +50 -7
- data/lib/toilet_tracker/parsers/message_parser.rb +64 -18
- data/lib/toilet_tracker/parsers/whatsapp_parser.rb +5 -5
- data/lib/toilet_tracker/services/shift_service.rb +2 -2
- data/lib/toilet_tracker/services/timezone_service.rb +4 -4
- data/lib/toilet_tracker/utils/zip_handler.rb +8 -8
- data/lib/toilet_tracker/version.rb +1 -1
- data/lib/toilet_tracker.rb +14 -14
- metadata +1 -2
- data/.standard.yml +0 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 64738532b1c80623a41ce64ead3709e4802702b03f87c593ccbc7edbeb1ab7a6
|
|
4
|
+
data.tar.gz: 18c6967a7280153c7e8b0022bb4e8d42e2a422870e3d0af92e5a5b57c23f57eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e45b15dc41c8d141284bd94abb2d650052722b02344830f8b06fcb264d7e2e0cb320ac2c845d11280eb3e1faa1cfa12ff2b670a65df4b5535f7b1b563517723
|
|
7
|
+
data.tar.gz: 86504e632326c8f0c21a6ee3b7845bc129a809e1573c6efc2796232eed6dacffd02037a718c556db233b7e64aee1ab6cbc69efa701b4a657a7429eba492eeb94
|
data/.rubocop.yml
CHANGED
|
@@ -1,37 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
- **
|
|
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 (
|
|
190
|
-
bundle exec
|
|
189
|
+
# Check code style (RuboCop)
|
|
190
|
+
bundle exec rubocop
|
|
191
191
|
|
|
192
192
|
# Auto-fix style issues
|
|
193
|
-
bundle exec
|
|
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 **
|
|
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
|
|
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 =
|
|
10
|
+
@default_timezone = "UTC"
|
|
11
11
|
@max_shift_hours = 12
|
|
12
12
|
@message_patterns = {
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
require
|
|
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
|
-
|
|
11
|
+
when Time, ActiveSupport::TimeWithZone
|
|
12
12
|
timestamp
|
|
13
|
-
|
|
13
|
+
when String
|
|
14
14
|
Time.parse(timestamp)
|
|
15
|
-
|
|
15
|
+
else
|
|
16
16
|
raise ArgumentError, "Invalid timestamp type: #{timestamp.class}"
|
|
17
|
-
|
|
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,
|
|
13
|
+
raise NotImplementedError, "Subclasses must implement #parse"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def valid?(input)
|
|
17
|
-
raise NotImplementedError,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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(
|
|
8
|
-
return Core::Result.failure(ArgumentError.new(
|
|
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
|
-
|
|
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?(
|
|
47
|
-
error = Core::InvalidMessageFormat.new(content,
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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(
|
|
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::
|
|
144
|
+
Core::ToiletShiftResult.new(
|
|
126
145
|
type: :timezone_set,
|
|
127
146
|
status: :ok,
|
|
128
147
|
message: message,
|
|
129
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
55
|
+
return "" if content.nil?
|
|
56
56
|
|
|
57
57
|
# Remove the "This message was edited" suffix
|
|
58
|
-
content.gsub(config.edited_message_suffix,
|
|
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
|
|
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
|
|
4
|
-
require
|
|
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 ==
|
|
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,
|
|
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
|
-
|
|
33
|
+
chat_entries.min_by { |entry| entry.name.count("/") }
|
|
34
|
+
else
|
|
35
35
|
chat_entries.first
|
|
36
|
-
|
|
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([
|
|
67
|
+
temp_file = Tempfile.new([ "chat_export", ".txt" ])
|
|
68
68
|
|
|
69
69
|
begin
|
|
70
70
|
Zip::File.open(@zip_path) do |_zip|
|
data/lib/toilet_tracker.rb
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "active_support/time"
|
|
4
4
|
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
11
|
-
require_relative
|
|
12
|
-
require_relative
|
|
13
|
-
require_relative
|
|
14
|
-
require_relative
|
|
15
|
-
require_relative
|
|
16
|
-
require_relative
|
|
17
|
-
require_relative
|
|
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.
|
|
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