toilet_tracker 0.1.1 β†’ 0.2.2

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: 64738532b1c80623a41ce64ead3709e4802702b03f87c593ccbc7edbeb1ab7a6
4
- data.tar.gz: 18c6967a7280153c7e8b0022bb4e8d42e2a422870e3d0af92e5a5b57c23f57eb
3
+ metadata.gz: 4af948bef38e4c8911b60d3bdc887c38b11593d687841f950f0472c7b5748a51
4
+ data.tar.gz: 8ea58aabc73391c82395c7aaa91af3b369f6ad2c2d1d7ad0f513d02ef9976d7f
5
5
  SHA512:
6
- metadata.gz: 1e45b15dc41c8d141284bd94abb2d650052722b02344830f8b06fcb264d7e2e0cb320ac2c845d11280eb3e1faa1cfa12ff2b670a65df4b5535f7b1b563517723
7
- data.tar.gz: 86504e632326c8f0c21a6ee3b7845bc129a809e1573c6efc2796232eed6dacffd02037a718c556db233b7e64aee1ab6cbc69efa701b4a657a7429eba492eeb94
6
+ metadata.gz: c11a69adeb79215f621857aa03782fd98518f7e36d2afa2e491f96e1b6f0d1ed94df24b3701a9282e680c01a7054450de894e9eecabf992b716c8c52949b0fd8
7
+ data.tar.gz: f0b54ca705f8b548225bcac1ac9d47803b27704a316b9b3a57414d9f5a9aa204eb7c231c88fcba1c87e10b0b801be70759467315b2f75cfca632778f91f5b6d8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.2] - 2026-09-03
4
+
5
+ ### Fixed
6
+ - Parse WhatsApp exports that use 4-digit years (`[02/01/2025, 23:27:21]`). The
7
+ message pattern required a 2-digit year, so current exports failed to parse
8
+ entirely: the only lines that matched were old-format timestamps quoted inside
9
+ message bodies, which produced a handful of events attributed to the quoted
10
+ sender. A 21,906-line export went from 3 parsed events to 13,996.
11
+ - Stop prefixing the century onto years that already have one (`2025` no longer
12
+ becomes `202025`).
13
+
14
+ ### Added
15
+ - Accept day and month without zero padding (`[2/1/2025, ...]`)
16
+ - Accept timestamps without seconds (`[02/01/2025, 23:27]`), defaulting to `:00`
17
+
18
+ ## [0.2.1] - 2025-12-18
19
+
20
+ ### Changed
21
+ - Update activesupport to 8.1.1 (from 8.0.2.1)
22
+ - Update rubyzip to 3.2.2 (from 3.0.1)
23
+ - Update rake to 13.3.1 (from 13.3.0)
24
+ - Update rspec to 3.13.2 (from 3.13.1)
25
+ - Update overcommit to 0.68.0 (from 0.67.1)
26
+ - Relax version constraints for activesupport and rubyzip
27
+
28
+ ## [0.2.0] - 2025-12-18
29
+
30
+ ### Added
31
+ - Metadata emoji support for both πŸ’© and 🚽 commands
32
+ - Format: `[command emoji] [metadata emojis...] [shift/timezone] [time]`
33
+ - Metadata stored as array of individual emoji strings
34
+ - New `metadata` field on `PoopEventResult` and `ToiletShiftResult`
35
+ - `BaseParser#parse_metadata` and `#metadata_emoji?` helper methods
36
+ - Comprehensive test suite for metadata parsing (28 new tests)
37
+
38
+ ### Changed
39
+ - All regex patterns updated to capture optional metadata group
40
+ - Result objects now include `metadata: []` by default (backward compatible)
41
+
3
42
  ## [0.1.1] - 2025-12-18
4
43
 
5
44
  ### Added
data/PARSING_SPEC.md CHANGED
@@ -9,8 +9,10 @@ ToiletTracker is a Ruby gem that parses WhatsApp message exports to track toilet
9
9
  ### Message Structure
10
10
  All WhatsApp messages follow this format:
11
11
  ```
12
- [DD/MM/YY, HH:MM:SS] Sender: Content
12
+ [DD/MM/YYYY, HH:MM:SS] Sender: Content
13
13
  ```
14
+ Exports vary by platform and locale. The year may be 2 or 4 digits, the day and
15
+ month may be zero-padded or not, and the seconds may be absent.
14
16
 
15
17
  ### Command Types
16
18
  - **🚽 (Settings)**: Configure timezone shifts and preferences
@@ -20,20 +22,45 @@ All WhatsApp messages follow this format:
20
22
  - **Functional Shifts**: Applied to event timestamps (patterns 1, 2, 5)
21
23
  - **Decorative Shifts**: For statistics only, do not affect timestamps (patterns 7, 9)
22
24
 
25
+ ### Metadata Emojis
26
+ Both πŸ’© and 🚽 commands support optional metadata emojis placed between the command emoji and the time/shift specifications. Metadata emojis are stored as an array and can be used for additional categorization or tagging of events.
27
+
28
+ **Format**: `[command emoji] [metadata emojis...] [shift/timezone] [time]`
29
+
30
+ **Examples**:
31
+ - `πŸ’© 🩸` β†’ Event with metadata `["🩸"]`
32
+ - `πŸ’© πŸ©ΈπŸ’§πŸ”₯` β†’ Event with metadata `["🩸", "πŸ’§", "πŸ”₯"]`
33
+ - `πŸ’© 🩸 +2` β†’ Event with metadata `["🩸"]` and shift `+2`
34
+ - `🚽 ✈️🌍 +5:30` β†’ Setting with metadata `["✈️", "🌍"]` and shift `+5:30`
35
+
36
+ **Behavior**:
37
+ - Metadata is any emoji placed after the command emoji but before time specs
38
+ - Emojis are extracted as individual grapheme clusters (supports complex emojis)
39
+ - Command emojis (πŸ’©, 🚽) are excluded from metadata
40
+ - Stored as an array of strings in result objects
41
+ - Does not affect parsing of shifts, timezones, or times
42
+
23
43
  ## Supported Parsing Patterns
24
44
 
25
45
  ### WhatsApp Message Format
26
- **Pattern**: `\[(?<day>\d{2})/(?<month>\d{2})/(?<year>\d{2}),\s*(?<time>\d{2}:\d{2}:\d{2})\]\s*(?<sender>[^:]+):\s*(?<content>.+?)(?:\s*β€Ž<This message was edited>)?$`
46
+ **Pattern**: `\[(?<day>\d{1,2})/(?<month>\d{1,2})/(?<year>\d{4}|\d{2}),\s*(?<time>\d{1,2}:\d{2}(?::\d{2})?)\]\s*(?<sender>[^:]+):\s*(?<content>.+?)(?:\s*β€Ž<This message was edited>)?$`
27
47
 
28
48
  **Examples**:
49
+ - `[01/01/2025, 12:00:00] Alice: πŸ’©`
29
50
  - `[01/01/25, 12:00:00] Alice: πŸ’©`
30
51
  - `[25/12/24, 23:59:59] Bob: 🚽 +2`
31
- - `[15/06/25, 14:30:15] Charlie: πŸ’© +1 2025-06-15 12:00:00`
52
+ - `[15/06/2025, 14:30:15] Charlie: πŸ’© +1 2025-06-15 12:00:00`
53
+ - `[2/1/2025, 9:05] Dave: πŸ’©`
32
54
 
33
55
  **Behavior**:
34
- - Extracts day, month, year (2-digit), time, sender, content
56
+ - Extracts day, month, year (2- or 4-digit), time, sender, content
35
57
  - Handles edited messages with suffix `β€Ž<This message was edited>`
36
- - Year is converted to 20XX format (25 β†’ 2025)
58
+ - A 2-digit year is converted to 20XX format (25 β†’ 2025); a 4-digit year is used as-is
59
+ - Seconds are optional and default to 0 when absent
60
+ - Day and month may be given with or without zero padding
61
+
62
+ **Not supported**: 12-hour timestamps with an AM/PM suffix, and the Android
63
+ export layout (`DD/MM/YYYY, HH:MM - Sender: Content`).
37
64
 
38
65
  ### 1. Shift Set (Functional)
39
66
  **Pattern**: `🚽\s*([+-]?\d+)$`
@@ -262,8 +289,9 @@ When timezone shifts are set retroactively:
262
289
  type: :shift_set | :shift_set_at_time | :timezone_set,
263
290
  status: :ok | :alert | :error,
264
291
  message: Core::Message,
265
- shift: Integer,
292
+ shift: Integer | Float,
266
293
  effective_time: Time,
294
+ metadata: Array<String>,
267
295
  error_details: String | nil
268
296
  }
269
297
  ```
@@ -275,9 +303,10 @@ When timezone shifts are set retroactively:
275
303
  :event_in_timezone | :event_in_timezone_at_time,
276
304
  status: :ok | :alert | :error,
277
305
  message: Core::Message,
278
- shift: Integer | nil,
306
+ shift: Integer | Float | nil,
279
307
  poop_time: Time,
280
308
  timezone: String | nil,
309
+ metadata: Array<String>,
281
310
  error_details: String | nil
282
311
  }
283
312
  ```
@@ -342,6 +371,16 @@ When timezone shifts are set retroactively:
342
371
  [02/01/25, 15:00:00] Bob: πŸ’© PST 2025-01-02 12:00:00
343
372
  ```
344
373
 
374
+ ### Valid Examples with Metadata
375
+ ```
376
+ [01/01/25, 08:30:00] Alice: πŸ’© 🩸 # metadata: ["🩸"]
377
+ [01/01/25, 10:00:00] Alice: πŸ’© πŸ©ΈπŸ’§ +2 # metadata: ["🩸", "πŸ’§"], shift: 2
378
+ [01/01/25, 12:00:00] Alice: πŸ’© 🧱 14:30 # metadata: ["🧱"], time: 14:30
379
+ [01/01/25, 14:00:00] Alice: πŸ’© πŸ”₯ +2 2025-01-01 13:00 # metadata + shift + time
380
+ [02/01/25, 09:00:00] Bob: 🚽 ✈️ +5:30 # metadata: ["✈️"], shift: 5.5
381
+ [02/01/25, 10:00:00] Bob: 🚽 🏠✈️ Europe/Rome # metadata: ["🏠", "✈️"], timezone
382
+ ```
383
+
345
384
  ### Invalid Examples
346
385
  ```
347
386
  [01/01/25, 10:00:00] Alice: 🚽 +25 # Exceeds max_shift_hours
data/README.md CHANGED
@@ -1,253 +1,106 @@
1
1
  # ToiletTracker
2
2
 
3
- A modern Ruby gem for parsing WhatsApp messages containing toilet tracking data using emoji-based commands. Built with clean architecture principles and comprehensive timezone support.
3
+ [![CI](https://github.com/BrownOps/toilet_tracker/actions/workflows/main.yml/badge.svg)](https://github.com/BrownOps/toilet_tracker/actions/workflows/main.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/toilet_tracker.svg)](https://rubygems.org/gems/toilet_tracker)
5
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.4-red.svg)](https://www.ruby-lang.org)
4
7
 
5
- ## Features
6
-
7
- - 🚽 **Settings**: Set timezone shifts and preferences
8
- - πŸ’© **Event Tracking**: Log events with automatic timezone adjustments
9
- - ⏰ **Timezone Support**: Handle multiple timezones and retroactive shifts
10
- - πŸ“Š **Multiple Output Formats**: JSON, CSV, and formatted tables
11
- - πŸ”§ **Thor CLI**: Interactive command-line interface with TTY::Prompt
12
- - πŸ§ͺ **Comprehensive Testing**: 77 tests with 96%+ coverage
13
- - πŸ—οΈ **Clean Architecture**: Modular design with clear separation of concerns
14
- - πŸ“‹ **Statistics**: Detailed parsing statistics and error reporting
15
-
16
- ## Requirements
8
+ Track bowel movements through WhatsApp messages using simple emoji commands. Export your WhatsApp chat, run it through ToiletTracker, and get structured data with timezone support.
17
9
 
18
- - **Ruby 3.4+** - Uses modern Ruby features for optimal performance
19
- - **RuboCop Omakase** - Opinionated Ruby styling from the Rails team
10
+ ## Features
20
11
 
21
- ## Installation
12
+ - **No app needed** - Use WhatsApp, which you already have
13
+ - **Travel-friendly** - Full timezone support for globetrotters
14
+ - **Flexible input** - Log now, log past events, add metadata
15
+ - **Multiple exports** - JSON, CSV, or formatted tables
16
+ - **Privacy first** - Your data stays on your device
22
17
 
23
- Add this line to your application's Gemfile:
18
+ ## Why?
24
19
 
25
- ```ruby
26
- gem 'toilet_tracker'
27
- ```
20
+ Tracking health data shouldn't require a dedicated app. WhatsApp is already on your phone, always synced, and easy to use. Just send a πŸ’© to a chat (or yourself) whenever nature calls, and this gem turns those messages into analyzable data.
28
21
 
29
- And then execute:
30
-
31
- ```bash
32
- bundle install
33
- ```
34
-
35
- Or install it yourself as:
22
+ ## Quick Start
36
23
 
37
24
  ```bash
38
25
  gem install toilet_tracker
39
26
  ```
40
27
 
41
- ## Usage
42
-
43
- ### Command Line Interface
44
-
45
- Process WhatsApp export files:
28
+ Export your WhatsApp chat and run:
46
29
 
47
30
  ```bash
48
- # Basic usage
49
- toilet_tracker parse whatsapp_export.txt
50
-
51
- # Parse WhatsApp zip exports (automatically extracts _chat.txt)
52
31
  toilet_tracker parse whatsapp_export.zip
53
-
54
- # JSON output
55
- toilet_tracker parse --format json messages.txt
56
-
57
- # Custom timezone
58
- toilet_tracker parse --timezone "Europe/Rome" messages.txt
59
-
60
- # Show statistics
61
- toilet_tracker stats messages.txt
62
-
63
- # Interactive mode
64
- toilet_tracker interactive
65
-
66
- # Show version
67
- toilet_tracker version
68
- ```
69
-
70
- ### Ruby API
71
-
72
- ```ruby
73
- require 'toilet_tracker'
74
-
75
- # Parse individual lines
76
- result = ToiletTracker.parse_line("[01/01/25, 12:00:00] Alice: πŸ’©")
77
- puts result.type # => :event_now
78
-
79
- # Parse multiple lines
80
- lines = [
81
- "[01/01/25, 10:00:00] Alice: 🚽 +2",
82
- "[01/01/25, 12:00:00] Alice: πŸ’©"
83
- ]
84
- results = ToiletTracker.parse_lines(lines)
85
-
86
- # Configure settings
87
- ToiletTracker.configure do |config|
88
- config.default_timezone = "Europe/Rome"
89
- config.max_shift_hours = 12
90
- end
91
32
  ```
92
33
 
93
- ### Supported Message Formats
94
-
95
- #### 🚽 Settings (Functional - Affect Timestamps)
96
- - `🚽 +2` - Set timezone shift to +2 hours (immediate)
97
- - `🚽 -1` - Set timezone shift to -1 hour (immediate)
98
- - `🚽 +1 2025-01-15 14:30` - Set shift effective from specific time (retroactive)
99
- - `🚽 Europe/Rome` - Set timezone by name
100
- - `🚽 PST` - Set timezone by abbreviation
101
-
102
- #### πŸ’© Events
103
-
104
- **Functional Shifts (Affect Actual Times):**
105
- - `πŸ’©` - Log live event with current active shift
106
- - `πŸ’© +1` - Log live event with +1 hour shift override
107
- - `πŸ’© PST` - Log live event converted to PST timezone
108
-
109
- **Fixed Times (No Shift Applied):**
110
- - `πŸ’© 2025-01-15 14:30` - Log event at exact time specified
34
+ ## How It Works
111
35
 
112
- **Decorative Data (For Statistics Only):**
113
- - `πŸ’© +2 2025-01-15 14:30` - Log event at 14:30 exactly, +2 shift stored for display
114
- - `πŸ’© EST 2025-01-15 14:30` - Log event at 14:30 exactly, EST timezone stored for display
36
+ Send emoji commands in any WhatsApp chat:
115
37
 
116
- > **Important**: In decorative patterns, the shift/timezone is stored for statistics but does NOT modify the actual poop timestamp.
38
+ | Command | What it does |
39
+ |---------|--------------|
40
+ | `πŸ’©` | Log an event right now |
41
+ | `πŸ’© 14:30` | Log an event at a specific time |
42
+ | `🚽 +2` | Set your timezone offset to +2 hours |
43
+ | `🚽 Europe/Rome` | Set timezone by name |
117
44
 
118
- ### Configuration Options
45
+ See the full [message specification](https://github.com/BrownOps/spec) for all supported formats.
119
46
 
120
- ```ruby
121
- ToiletTracker.configure do |config|
122
- # Default timezone for parsing timestamps
123
- config.default_timezone = "UTC"
124
-
125
- # Maximum allowed shift hours
126
- config.max_shift_hours = 12
127
-
128
- # Custom message patterns (advanced)
129
- config.message_patterns[:custom] = /custom_pattern/
130
-
131
- # Supported date formats
132
- config.date_formats = ["%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M"]
133
- end
134
- ```
135
-
136
- ## Examples
47
+ ### Timezone Support
137
48
 
138
- ### Basic Usage Scenarios
49
+ Traveling? Just update your timezone:
139
50
 
140
- #### Setting Up Timezone Shifts
141
51
  ```
142
- [15/01/25, 10:00:00] Alice: 🚽 +2
143
- [15/01/25, 12:00:00] Alice: πŸ’©
144
- # Result: Event logged at 14:00 (12:00 + 2 hours)
52
+ 🚽 +5:30 # Flying to India
53
+ πŸ’© # Logged with +5:30 offset
54
+ 🚽 Europe/Rome # Back home
55
+ πŸ’© # Logged with Rome timezone
145
56
  ```
146
57
 
147
- #### Retroactive Timezone Adjustments
148
- ```
149
- [15/01/25, 10:00:00] Alice: πŸ’©
150
- [15/01/25, 14:00:00] Alice: 🚽 +3 2025-01-15 09:00:00
151
- # Result: First event adjusted to 13:00 (10:00 + 3 hours)
152
- ```
58
+ ### Metadata
153
59
 
154
- #### Decorative vs Functional Shifts
155
- ```
156
- # Functional: Affects timestamp
157
- [15/01/25, 12:00:00] Alice: πŸ’© +2
158
- # Result: Event at 14:00 (12:00 + 2 hours)
159
-
160
- # Decorative: Only for statistics
161
- [15/01/25, 12:00:00] Alice: πŸ’© +2 2025-01-15 10:00:00
162
- # Result: Event at exactly 10:00, +2 stored for display
163
- ```
60
+ Add extra context with additional emojis:
164
61
 
165
- #### Complex Multi-User Scenario
166
62
  ```
167
- [15/01/25, 09:00:00] Alice: 🚽 +2
168
- [15/01/25, 09:30:00] Bob: 🚽 Europe/Rome
169
- [15/01/25, 10:00:00] Alice: πŸ’©
170
- [15/01/25, 10:30:00] Bob: πŸ’©
171
- [15/01/25, 11:00:00] Alice: πŸ’© +1
172
- [15/01/25, 11:30:00] Bob: πŸ’© EST 2025-01-15 08:00:00
63
+ πŸ’© 🩸 # Flag something unusual
64
+ πŸ’© 🧱 # Note the consistency
65
+ πŸ’© β˜• πŸ• # Track what you ate
173
66
  ```
174
67
 
175
- Results:
176
- - Alice's first event: 12:00 (10:00 + 2 hours active shift)
177
- - Bob's first event: ~11:30 (10:30 converted from UTC to Rome time)
178
- - Alice's override event: 12:00 (11:00 + 1 hour override)
179
- - Bob's decorative event: exactly 08:00 (EST is decorative only)
180
-
181
- ## Development
182
-
183
- After checking out the repo, run `bin/setup` to install dependencies. Then, run the following commands:
68
+ ## Output Formats
184
69
 
185
70
  ```bash
186
- # Run tests
187
- bundle exec rspec
188
-
189
- # Check code style (RuboCop)
190
- bundle exec rubocop
71
+ # Table (default)
72
+ toilet_tracker parse chat.zip
191
73
 
192
- # Auto-fix style issues
193
- bundle exec rubocop -a
74
+ # JSON for further processing
75
+ toilet_tracker parse --format json chat.zip
194
76
 
195
- # Install gem locally
196
- bundle exec rake install
77
+ # CSV for spreadsheets
78
+ toilet_tracker parse --format csv chat.zip
197
79
  ```
198
80
 
199
- ### Code Quality
81
+ ## Ruby API
200
82
 
201
- This project uses **RuboCop Omakase** for opinionated Ruby linting and formatting. Omakase provides the Rails team's preferred styling rules.
202
-
203
- Key principles:
204
- - **Ruby 3.4+ features** - Modern syntax and performance optimizations
205
- - **KISS principle** - Keep code simple and readable
206
- - **Test-driven development** - Comprehensive RSpec test suite
207
- - **Clean architecture** - Clear separation of concerns
208
-
209
- ### Creating a Release
210
-
211
- To release a new version:
212
-
213
- 1. **Update the version** in `lib/toilet_tracker/version.rb`:
214
- ```ruby
215
- module ToiletTracker
216
- VERSION = "1.2.3"
217
- end
218
- ```
219
-
220
- 2. **Update CHANGELOG.md** with release notes (optional but recommended)
83
+ ```ruby
84
+ require 'toilet_tracker'
221
85
 
222
- 3. **Commit your changes**:
223
- ```bash
224
- git add -A
225
- git commit -m "Bump version to 1.2.3"
226
- ```
86
+ # Parse a WhatsApp export
87
+ results = ToiletTracker.parse_file("chat.txt")
227
88
 
228
- 4. **Create and push a git tag**:
229
- ```bash
230
- git tag v1.2.3
231
- git push origin main --tags
232
- ```
89
+ # Or parse individual messages
90
+ result = ToiletTracker.parse_line("[01/01/2025, 12:00:00] Me: πŸ’©")
91
+ ```
233
92
 
234
- 5. **Automated Release Process**: The GitHub Actions workflow will automatically:
235
- - Run tests and linting to validate the release
236
- - Build the gem
237
- - Publish to RubyGems.org (requires `RUBYGEMS_API_KEY` secret)
238
- - Create a GitHub release with auto-generated changelog
239
- - Upload the gem file as a release asset
93
+ ## Requirements
240
94
 
241
- **Note**: Ensure the `RUBYGEMS_API_KEY` secret is configured in your GitHub repository settings for automatic publishing to work.
95
+ - Ruby 3.4+
242
96
 
243
- ## Contributing
97
+ ## Development
244
98
 
245
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/toilet_tracker. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/toilet_tracker/blob/main/CODE_OF_CONDUCT.md).
99
+ ```bash
100
+ bin/setup # Install dependencies
101
+ bundle exec rspec # Run tests
102
+ ```
246
103
 
247
104
  ## License
248
105
 
249
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
250
-
251
- ## Code of Conduct
252
-
253
- Everyone interacting in the ToiletTracker project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/toilet_tracker/blob/main/CODE_OF_CONDUCT.md).
106
+ MIT - see [LICENSE.txt](LICENSE.txt)
@@ -9,37 +9,38 @@ module ToiletTracker
9
9
  def initialize
10
10
  @default_timezone = "UTC"
11
11
  @max_shift_hours = 12
12
+ # Capture group 1 is always metadata emojis, subsequent groups are pattern-specific
12
13
  @message_patterns = {
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
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
- # πŸ’©
32
- event_now: /πŸ’©$/,
33
- # πŸ’© +2, πŸ’© -1, πŸ’© +5:30
34
- event_now_shifted: /πŸ’©\s*([+-]?\d+(?::\d{2})?)$/,
35
- # πŸ’© 2025-01-15 14:30
36
- event_at_time: %r{πŸ’©\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
40
- event_in_timezone: %r{πŸ’©\s*([A-Za-z_/][A-Za-z0-9_/+-]*)\s*$},
41
- # πŸ’© Asia/Kolkata 2025-01-15 14:30
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})?)$}
14
+ # πŸ’© 🩸 14:30
15
+ event_at_time_only: /πŸ’©\s*([^+\-\dA-Za-z\s]*)\s*(\d{1,2}:\d{2})$/,
16
+ # πŸ’© 🩸 +5 14:30, πŸ’© 🩸 +5:30 14:30
17
+ event_at_time_only_shifted: /πŸ’©\s*([^+\-\dA-Za-z\s]*)\s*([+-]?\d+(?::\d{2})?)\s+(\d{1,2}:\d{2})$/,
18
+ # πŸ’© 🩸 Asia/Kolkata 14:30
19
+ event_in_timezone_at_time_only: %r{πŸ’©\s*([^+\-\dA-Za-z\s]*)\s*([A-Za-z_/][A-Za-z0-9_/+-]*)\s+(\d{1,2}:\d{2})$},
20
+ # 🚽 πŸ“ +5 14:30, 🚽 πŸ“ +5:30 14:30
21
+ shift_set_at_time_only: /🚽\s*([^+\-\dA-Za-z\s]*)\s*([+-]?\d+(?::\d{2})?)\s+(\d{1,2}:\d{2})$/,
22
+ # 🚽 πŸ“ Asia/Kolkata 14:30
23
+ timezone_set_at_time_only: %r{🚽\s*([^+\-\dA-Za-z\s]*)\s*([A-Za-z_/][A-Za-z0-9_/+-]*)\s+(\d{1,2}:\d{2})$},
24
+ # 🚽 πŸ“ +2, 🚽 πŸ“ -3, 🚽 πŸ“ +5:30
25
+ shift_set: /🚽\s*([^+\-\dA-Za-z\s]*)\s*([+-]?\d+(?::\d{2})?)$/,
26
+ # 🚽 πŸ“ +2 2025-01-15 14:30, 🚽 πŸ“ +5:30 2025-01-15 14:30
27
+ shift_set_at_time: %r{🚽\s*([^+\-\dA-Za-z\s]*)\s*([+-]?\d+(?::\d{2})?)\s+(\d{4}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}:\d{1,2}(?::\d{1,2})?)$},
28
+ # 🚽 πŸ“ Europe/Rome, 🚽 πŸ“ PST
29
+ timezone_set: %r{🚽\s*([^+\-\dA-Za-z\s]*)\s*([A-Za-z_/][A-Za-z0-9_/+-]*)$},
30
+ # 🚽 πŸ“ Asia/Kolkata 2025-01-15 14:30
31
+ timezone_set_at_time: %r{🚽\s*([^+\-\dA-Za-z\s]*)\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})?)$},
32
+ # πŸ’©, πŸ’© 🩸
33
+ event_now: /πŸ’©\s*([^+\-\dA-Za-z\s]*)$/,
34
+ # πŸ’© 🩸 +2, πŸ’© 🩸 -1, πŸ’© 🩸 +5:30
35
+ event_now_shifted: /πŸ’©\s*([^+\-\dA-Za-z\s]*)\s*([+-]?\d+(?::\d{2})?)$/,
36
+ # πŸ’© 🩸 2025-01-15 14:30
37
+ event_at_time: %r{πŸ’©\s*([^+\-\dA-Za-z\s]*)\s*(\d{4}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}:\d{1,2}(?::\d{1,2})?)$},
38
+ # πŸ’© 🩸 +2 2025-01-15 14:30, πŸ’© 🩸 +5:30 2025-01-15 14:30
39
+ event_at_time_shifted: %r{πŸ’©\s*([^+\-\dA-Za-z\s]*)\s*([+-]?\d+(?::\d{2})?)\s+(\d{4}[-/]\d{1,2}[-/]\d{1,2}\s+\d{1,2}:\d{1,2}(?::\d{1,2})?)$},
40
+ # πŸ’© 🩸 Europe/Rome, πŸ’© 🩸 PST
41
+ event_in_timezone: %r{πŸ’©\s*([^+\-\dA-Za-z\s]*)\s*([A-Za-z_/][A-Za-z0-9_/+-]*)\s*$},
42
+ # πŸ’© 🩸 Asia/Kolkata 2025-01-15 14:30
43
+ event_in_timezone_at_time: %r{πŸ’©\s*([^+\-\dA-Za-z\s]*)\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})?)$}
43
44
  }
44
45
  @date_formats = [
45
46
  "%Y-%m-%d %H:%M:%S",
@@ -60,7 +61,9 @@ module ToiletTracker
60
61
  end
61
62
 
62
63
  def whatsapp_message_pattern
63
- @whatsapp_message_pattern ||= %r{\[(?<day>\d{2})/(?<month>\d{2})/(?<year>\d{2}),\s*(?<time>\d{2}:\d{2}:\d{2})\]\s*(?<sender>[^:]+):\s*(?<content>.+?)(?:\s*β€Ž<This message was edited>)?$}
64
+ # WhatsApp exports vary by platform and locale: years are either 2 or 4
65
+ # digits, day and month may be zero-padded or not, and seconds may be absent.
66
+ @whatsapp_message_pattern ||= %r{\[(?<day>\d{1,2})/(?<month>\d{1,2})/(?<year>\d{4}|\d{2}),\s*(?<time>\d{1,2}:\d{2}(?::\d{2})?)\]\s*(?<sender>[^:]+):\s*(?<content>.+?)(?:\s*β€Ž<This message was edited>)?$}
64
67
  end
65
68
 
66
69
  def edited_message_suffix
@@ -22,15 +22,15 @@ module ToiletTracker
22
22
  end
23
23
 
24
24
  # Result for toilet shift settings
25
- ToiletShiftResult = Data.define(:type, :status, :message, :shift, :effective_time, :error_details) do
26
- def initialize(type:, status:, message:, shift: nil, effective_time: nil, error_details: nil)
25
+ ToiletShiftResult = Data.define(:type, :status, :message, :shift, :effective_time, :metadata, :error_details) do
26
+ def initialize(type:, status:, message:, shift: nil, effective_time: nil, metadata: [], error_details: nil)
27
27
  super
28
28
  end
29
29
  end
30
30
 
31
31
  # Result for poop events
32
- PoopEventResult = Data.define(:type, :status, :message, :shift, :poop_time, :timezone, :error_details) do
33
- def initialize(type:, status:, message:, shift: nil, poop_time: nil, timezone: nil, error_details: nil)
32
+ PoopEventResult = Data.define(:type, :status, :message, :shift, :poop_time, :timezone, :metadata, :error_details) do
33
+ def initialize(type:, status:, message:, shift: nil, poop_time: nil, timezone: nil, metadata: [], error_details: nil)
34
34
  super
35
35
  end
36
36
  end
@@ -98,6 +98,21 @@ module ToiletTracker
98
98
  time_diff = (message_time - event_time).abs
99
99
  (time_diff > 30.days) ? :alert : :ok
100
100
  end
101
+
102
+ def parse_metadata(metadata_string)
103
+ return [] if metadata_string.nil? || metadata_string.strip.empty?
104
+
105
+ metadata_string.grapheme_clusters
106
+ .reject { |char| char.match?(/\s/) }
107
+ .select { |char| metadata_emoji?(char) }
108
+ end
109
+
110
+ def metadata_emoji?(char)
111
+ return false if char.match?(/\A[\p{ASCII}]\z/)
112
+ return false if %w[πŸ’© 🚽].include?(char)
113
+
114
+ char.match?(/\p{Emoji}/) && !char.match?(/\A\d\z/)
115
+ end
101
116
  end
102
117
  end
103
118
  end
@@ -96,7 +96,7 @@ module ToiletTracker
96
96
  def parse_poop_type(type, match, message)
97
97
  case type
98
98
  when :event_now
99
- parse_poop_live(message)
99
+ parse_poop_live(match, message)
100
100
  when :event_now_shifted
101
101
  parse_poop_live_shift(match, message)
102
102
  when :event_at_time
@@ -111,20 +111,23 @@ module ToiletTracker
111
111
  end
112
112
 
113
113
  def parse_toilet_shift(match, message)
114
- shift = parse_shift(match[1])
114
+ metadata = parse_metadata(match[1])
115
+ shift = parse_shift(match[2])
115
116
 
116
117
  Core::ToiletShiftResult.new(
117
118
  type: :shift_set,
118
119
  status: :ok,
119
120
  message: message,
120
121
  shift: shift,
121
- effective_time: message.timestamp
122
+ effective_time: message.timestamp,
123
+ metadata: metadata
122
124
  )
123
125
  end
124
126
 
125
127
  def parse_toilet_shift_with_time(match, message)
126
- shift = parse_shift(match[1])
127
- effective_time = parse_datetime(match[2], @current_message)
128
+ metadata = parse_metadata(match[1])
129
+ shift = parse_shift(match[2])
130
+ effective_time = parse_datetime(match[3], @current_message)
128
131
 
129
132
  status = determine_status(effective_time, message.timestamp)
130
133
 
@@ -133,12 +136,14 @@ module ToiletTracker
133
136
  status: status,
134
137
  message: message,
135
138
  shift: shift,
136
- effective_time: effective_time
139
+ effective_time: effective_time,
140
+ metadata: metadata
137
141
  )
138
142
  end
139
143
 
140
144
  def parse_toilet_timezone(match, message)
141
- timezone_str = match[1]
145
+ metadata = parse_metadata(match[1])
146
+ timezone_str = match[2]
142
147
  shift = calculate_shift_from_timezone(timezone_str)
143
148
 
144
149
  Core::ToiletShiftResult.new(
@@ -146,13 +151,15 @@ module ToiletTracker
146
151
  status: :ok,
147
152
  message: message,
148
153
  shift: shift,
149
- effective_time: message.timestamp
154
+ effective_time: message.timestamp,
155
+ metadata: metadata
150
156
  )
151
157
  end
152
158
 
153
159
  def parse_toilet_timezone_with_time(match, message)
154
- timezone_str = match[1]
155
- datetime_str = match[2]
160
+ metadata = parse_metadata(match[1])
161
+ timezone_str = match[2]
162
+ datetime_str = match[3]
156
163
 
157
164
  effective_time = parse_datetime(datetime_str, @current_message)
158
165
  status = determine_status(effective_time, message.timestamp)
@@ -163,7 +170,8 @@ module ToiletTracker
163
170
  status: status,
164
171
  message: message,
165
172
  shift: shift,
166
- effective_time: effective_time
173
+ effective_time: effective_time,
174
+ metadata: metadata
167
175
  )
168
176
  end
169
177
 
@@ -176,30 +184,36 @@ module ToiletTracker
176
184
  (shift == shift.to_i) ? shift.to_i : shift
177
185
  end
178
186
 
179
- def parse_poop_live(message)
187
+ def parse_poop_live(match, message)
188
+ metadata = parse_metadata(match[1])
189
+
180
190
  Core::PoopEventResult.new(
181
191
  type: :event_now,
182
192
  status: :ok,
183
193
  message: message,
184
194
  shift: 0,
185
- poop_time: message.timestamp
195
+ poop_time: message.timestamp,
196
+ metadata: metadata
186
197
  )
187
198
  end
188
199
 
189
200
  def parse_poop_live_shift(match, message)
190
- shift = parse_shift(match[1])
201
+ metadata = parse_metadata(match[1])
202
+ shift = parse_shift(match[2])
191
203
 
192
204
  Core::PoopEventResult.new(
193
205
  type: :event_now_shifted,
194
206
  status: :ok,
195
207
  message: message,
196
208
  shift: shift,
197
- poop_time: message.timestamp
209
+ poop_time: message.timestamp,
210
+ metadata: metadata
198
211
  )
199
212
  end
200
213
 
201
214
  def parse_poop_past(match, message)
202
- poop_time = parse_datetime(match[1], @current_message)
215
+ metadata = parse_metadata(match[1])
216
+ poop_time = parse_datetime(match[2], @current_message)
203
217
  status = determine_status(poop_time, message.timestamp)
204
218
 
205
219
  Core::PoopEventResult.new(
@@ -207,13 +221,15 @@ module ToiletTracker
207
221
  status: status,
208
222
  message: message,
209
223
  shift: 0,
210
- poop_time: poop_time
224
+ poop_time: poop_time,
225
+ metadata: metadata
211
226
  )
212
227
  end
213
228
 
214
229
  def parse_poop_past_with_shift(match, message)
215
- shift = parse_shift(match[1])
216
- poop_time = parse_datetime(match[2], @current_message)
230
+ metadata = parse_metadata(match[1])
231
+ shift = parse_shift(match[2])
232
+ poop_time = parse_datetime(match[3], @current_message)
217
233
  status = determine_status(poop_time, message.timestamp)
218
234
 
219
235
  Core::PoopEventResult.new(
@@ -221,25 +237,29 @@ module ToiletTracker
221
237
  status: status,
222
238
  message: message,
223
239
  shift: shift,
224
- poop_time: poop_time
240
+ poop_time: poop_time,
241
+ metadata: metadata
225
242
  )
226
243
  end
227
244
 
228
245
  def parse_poop_timezone(match, message)
229
- timezone_str = match[1]
246
+ metadata = parse_metadata(match[1])
247
+ timezone_str = match[2]
230
248
 
231
249
  Core::PoopEventResult.new(
232
250
  type: :event_in_timezone,
233
251
  status: :ok,
234
252
  message: message,
235
253
  poop_time: message.timestamp,
236
- timezone: timezone_str
254
+ timezone: timezone_str,
255
+ metadata: metadata
237
256
  )
238
257
  end
239
258
 
240
259
  def parse_poop_timezone_with_time(match, message)
241
- timezone_str = match[1]
242
- poop_time = parse_datetime(match[2], @current_message)
260
+ metadata = parse_metadata(match[1])
261
+ timezone_str = match[2]
262
+ poop_time = parse_datetime(match[3], @current_message)
243
263
  status = determine_status(poop_time, message.timestamp)
244
264
 
245
265
  Core::PoopEventResult.new(
@@ -247,7 +267,8 @@ module ToiletTracker
247
267
  status: status,
248
268
  message: message,
249
269
  poop_time: poop_time,
250
- timezone: timezone_str
270
+ timezone: timezone_str,
271
+ metadata: metadata
251
272
  )
252
273
  end
253
274
  end
@@ -41,16 +41,23 @@ module ToiletTracker
41
41
  end
42
42
 
43
43
  def parse_whatsapp_timestamp(day, month, year, time)
44
- # Convert YY to full year (assuming 2000s)
45
- full_year = "20#{year}"
46
- datetime_string = "#{day}/#{month}/#{full_year} #{time}"
44
+ datetime_string = "#{day}/#{month}/#{expand_year(year)} #{time}"
47
45
 
48
46
  # Parse in the default timezone
49
- config.default_timezone_object.strptime(datetime_string, "%d/%m/%Y %H:%M:%S")
47
+ config.default_timezone_object.strptime(datetime_string, timestamp_format(time))
50
48
  rescue ArgumentError => e
51
49
  raise Core::InvalidDateTime, "#{day}/#{month}/#{year} #{time}: #{e.message}"
52
50
  end
53
51
 
52
+ # Convert YY to full year (assuming 2000s), leave YYYY untouched
53
+ def expand_year(year)
54
+ year.length == 2 ? "20#{year}" : year
55
+ end
56
+
57
+ def timestamp_format(time)
58
+ time.count(":") > 1 ? "%d/%m/%Y %H:%M:%S" : "%d/%m/%Y %H:%M"
59
+ end
60
+
54
61
  def clean_content(content)
55
62
  return "" if content.nil?
56
63
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ToiletTracker
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.2"
5
5
  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.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - NetherSoul
@@ -13,16 +13,22 @@ dependencies:
13
13
  name: activesupport
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
18
  version: 8.0.2.1
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: 8.2.0
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
22
25
  requirements:
23
- - - "~>"
26
+ - - ">="
24
27
  - !ruby/object:Gem::Version
25
28
  version: 8.0.2.1
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: 8.2.0
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: csv
28
34
  requirement: !ruby/object:Gem::Requirement
@@ -41,16 +47,22 @@ dependencies:
41
47
  name: rubyzip
42
48
  requirement: !ruby/object:Gem::Requirement
43
49
  requirements:
44
- - - "~>"
50
+ - - ">="
45
51
  - !ruby/object:Gem::Version
46
52
  version: 3.0.1
53
+ - - "<"
54
+ - !ruby/object:Gem::Version
55
+ version: 3.3.0
47
56
  type: :runtime
48
57
  prerelease: false
49
58
  version_requirements: !ruby/object:Gem::Requirement
50
59
  requirements:
51
- - - "~>"
60
+ - - ">="
52
61
  - !ruby/object:Gem::Version
53
62
  version: 3.0.1
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: 3.3.0
54
66
  - !ruby/object:Gem::Dependency
55
67
  name: thor
56
68
  requirement: !ruby/object:Gem::Requirement
@@ -152,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
164
  - !ruby/object:Gem::Version
153
165
  version: '0'
154
166
  requirements: []
155
- rubygems_version: 3.6.7
167
+ rubygems_version: 4.0.3
156
168
  specification_version: 4
157
169
  summary: Parse WhatsApp messages for toilet tracking with emoji-based commands
158
170
  test_files: []