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 +4 -4
- data/CHANGELOG.md +39 -0
- data/PARSING_SPEC.md +46 -7
- data/README.md +58 -205
- data/lib/toilet_tracker/configuration.rb +34 -31
- data/lib/toilet_tracker/core/parse_result.rb +4 -4
- data/lib/toilet_tracker/parsers/base_parser.rb +15 -0
- data/lib/toilet_tracker/parsers/message_parser.rb +46 -25
- data/lib/toilet_tracker/parsers/whatsapp_parser.rb +11 -4
- data/lib/toilet_tracker/version.rb +1 -1
- metadata +18 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4af948bef38e4c8911b60d3bdc887c38b11593d687841f950f0472c7b5748a51
|
|
4
|
+
data.tar.gz: 8ea58aabc73391c82395c7aaa91af3b369f6ad2c2d1d7ad0f513d02ef9976d7f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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/
|
|
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}
|
|
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/
|
|
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
|
-
-
|
|
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
|
-
|
|
3
|
+
[](https://github.com/BrownOps/toilet_tracker/actions/workflows/main.yml)
|
|
4
|
+
[](https://rubygems.org/gems/toilet_tracker)
|
|
5
|
+
[](LICENSE.txt)
|
|
6
|
+
[](https://www.ruby-lang.org)
|
|
4
7
|
|
|
5
|
-
|
|
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
|
-
|
|
19
|
-
- **RuboCop Omakase** - Opinionated Ruby styling from the Rails team
|
|
10
|
+
## Features
|
|
20
11
|
|
|
21
|
-
|
|
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
|
-
|
|
18
|
+
## Why?
|
|
24
19
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
+
See the full [message specification](https://github.com/BrownOps/spec) for all supported formats.
|
|
119
46
|
|
|
120
|
-
|
|
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
|
-
|
|
49
|
+
Traveling? Just update your timezone:
|
|
139
50
|
|
|
140
|
-
#### Setting Up Timezone Shifts
|
|
141
51
|
```
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
# Check code style (RuboCop)
|
|
190
|
-
bundle exec rubocop
|
|
71
|
+
# Table (default)
|
|
72
|
+
toilet_tracker parse chat.zip
|
|
191
73
|
|
|
192
|
-
#
|
|
193
|
-
|
|
74
|
+
# JSON for further processing
|
|
75
|
+
toilet_tracker parse --format json chat.zip
|
|
194
76
|
|
|
195
|
-
#
|
|
196
|
-
|
|
77
|
+
# CSV for spreadsheets
|
|
78
|
+
toilet_tracker parse --format csv chat.zip
|
|
197
79
|
```
|
|
198
80
|
|
|
199
|
-
|
|
81
|
+
## Ruby API
|
|
200
82
|
|
|
201
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
+
- Ruby 3.4+
|
|
242
96
|
|
|
243
|
-
##
|
|
97
|
+
## Development
|
|
244
98
|
|
|
245
|
-
|
|
99
|
+
```bash
|
|
100
|
+
bin/setup # Install dependencies
|
|
101
|
+
bundle exec rspec # Run tests
|
|
102
|
+
```
|
|
246
103
|
|
|
247
104
|
## License
|
|
248
105
|
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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:
|
|
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: []
|