toilet_tracker 0.1.0
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 +7 -0
- data/.overcommit.yml +31 -0
- data/.rspec +3 -0
- data/.rubocop.yml +37 -0
- data/.standard.yml +2 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/PARSING_SPEC.md +361 -0
- data/README.md +253 -0
- data/Rakefile +12 -0
- data/STYLE_GUIDE.md +377 -0
- data/exe/toilet_tracker +419 -0
- data/lib/toilet_tracker/configuration.rb +64 -0
- data/lib/toilet_tracker/core/errors.rb +56 -0
- data/lib/toilet_tracker/core/message.rb +41 -0
- data/lib/toilet_tracker/core/parse_result.rb +45 -0
- data/lib/toilet_tracker/core/result.rb +46 -0
- data/lib/toilet_tracker/parsers/base_parser.rb +60 -0
- data/lib/toilet_tracker/parsers/message_parser.rb +209 -0
- data/lib/toilet_tracker/parsers/whatsapp_parser.rb +62 -0
- data/lib/toilet_tracker/services/analysis_service.rb +212 -0
- data/lib/toilet_tracker/services/shift_service.rb +89 -0
- data/lib/toilet_tracker/services/timezone_service.rb +93 -0
- data/lib/toilet_tracker/utils/zip_handler.rb +85 -0
- data/lib/toilet_tracker/version.rb +5 -0
- data/lib/toilet_tracker.rb +36 -0
- data/sig/toilet_tracker.rbs +4 -0
- metadata +159 -0
data/README.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# ToiletTracker
|
|
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.
|
|
4
|
+
|
|
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
|
|
17
|
+
|
|
18
|
+
- **Ruby 3.4+** - Uses modern Ruby features for optimal performance
|
|
19
|
+
- **StandardRB** - Zero-configuration linting and formatting
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Add this line to your application's Gemfile:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem 'toilet_tracker'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
And then execute:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bundle install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or install it yourself as:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
gem install toilet_tracker
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### Command Line Interface
|
|
44
|
+
|
|
45
|
+
Process WhatsApp export files:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Basic usage
|
|
49
|
+
toilet_tracker parse whatsapp_export.txt
|
|
50
|
+
|
|
51
|
+
# Parse WhatsApp zip exports (automatically extracts _chat.txt)
|
|
52
|
+
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
|
+
```
|
|
92
|
+
|
|
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
|
|
111
|
+
|
|
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
|
|
115
|
+
|
|
116
|
+
> **Important**: In decorative patterns, the shift/timezone is stored for statistics but does NOT modify the actual poop timestamp.
|
|
117
|
+
|
|
118
|
+
### Configuration Options
|
|
119
|
+
|
|
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
|
|
137
|
+
|
|
138
|
+
### Basic Usage Scenarios
|
|
139
|
+
|
|
140
|
+
#### Setting Up Timezone Shifts
|
|
141
|
+
```
|
|
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)
|
|
145
|
+
```
|
|
146
|
+
|
|
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
|
+
```
|
|
153
|
+
|
|
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
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Complex Multi-User Scenario
|
|
166
|
+
```
|
|
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
|
|
173
|
+
```
|
|
174
|
+
|
|
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:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Run tests
|
|
187
|
+
bundle exec rspec
|
|
188
|
+
|
|
189
|
+
# Check code style (StandardRB)
|
|
190
|
+
bundle exec standardrb
|
|
191
|
+
|
|
192
|
+
# Auto-fix style issues
|
|
193
|
+
bundle exec standardrb --fix
|
|
194
|
+
|
|
195
|
+
# Install gem locally
|
|
196
|
+
bundle exec rake install
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Code Quality
|
|
200
|
+
|
|
201
|
+
This project uses **StandardRB** for zero-configuration Ruby linting and formatting. StandardRB eliminates style debates by providing an opinionated, unconfigurable set of rules.
|
|
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)
|
|
221
|
+
|
|
222
|
+
3. **Commit your changes**:
|
|
223
|
+
```bash
|
|
224
|
+
git add -A
|
|
225
|
+
git commit -m "Bump version to 1.2.3"
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
4. **Create and push a git tag**:
|
|
229
|
+
```bash
|
|
230
|
+
git tag v1.2.3
|
|
231
|
+
git push origin main --tags
|
|
232
|
+
```
|
|
233
|
+
|
|
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
|
|
240
|
+
|
|
241
|
+
**Note**: Ensure the `RUBYGEMS_API_KEY` secret is configured in your GitHub repository settings for automatic publishing to work.
|
|
242
|
+
|
|
243
|
+
## Contributing
|
|
244
|
+
|
|
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).
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
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).
|
data/Rakefile
ADDED
data/STYLE_GUIDE.md
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# ToiletTracker Ruby Style Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This project follows Ruby 3.4 best practices with StandardRB for consistent code formatting. The guide emphasizes KISS (Keep It Simple, Stupid) principles and modern Ruby idioms.
|
|
6
|
+
|
|
7
|
+
## Ruby Version & Features
|
|
8
|
+
|
|
9
|
+
**Target Ruby Version**: 3.4+
|
|
10
|
+
|
|
11
|
+
### Ruby 3.4 Modern Features
|
|
12
|
+
|
|
13
|
+
#### Use `it` for Single Block Parameters
|
|
14
|
+
```ruby
|
|
15
|
+
# โ
Preferred (Ruby 3.4+)
|
|
16
|
+
[1, 2, 3].map { it * 2 }
|
|
17
|
+
results.filter { it.success? }
|
|
18
|
+
|
|
19
|
+
# โ Avoid (outdated)
|
|
20
|
+
[1, 2, 3].map { _1 * 2 }
|
|
21
|
+
results.filter { _1.success? }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
#### Frozen Strings by Default
|
|
25
|
+
```ruby
|
|
26
|
+
# โ
Remove these comments (redundant in Ruby 3.4)
|
|
27
|
+
# frozen_string_literal: true
|
|
28
|
+
|
|
29
|
+
# Ruby 3.4 makes strings frozen by default
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
#### Modern Hash Syntax
|
|
33
|
+
```ruby
|
|
34
|
+
# โ
Preferred - Symbol keys without fat arrow
|
|
35
|
+
user_data = { name: "Alice", age: 30, active: true }
|
|
36
|
+
|
|
37
|
+
# โ
Other keys with spaces around fat arrow
|
|
38
|
+
mixed_data = { "user-id" => 123, :name => "Alice" }
|
|
39
|
+
|
|
40
|
+
# โ Avoid - Old symbol syntax
|
|
41
|
+
user_data = { :name => "Alice", :age => 30 }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Code Style (StandardRB)
|
|
45
|
+
|
|
46
|
+
### Linting & Formatting
|
|
47
|
+
```bash
|
|
48
|
+
# Check code style
|
|
49
|
+
bundle exec standardrb
|
|
50
|
+
|
|
51
|
+
# Auto-fix issues
|
|
52
|
+
bundle exec standardrb --fix
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Configuration
|
|
56
|
+
StandardRB is **unconfigurable by design** to eliminate style debates. The `.standard.yml` file contains:
|
|
57
|
+
```yaml
|
|
58
|
+
ruby_version: 3.4
|
|
59
|
+
fix: true
|
|
60
|
+
ignore:
|
|
61
|
+
- 'vendor/**/*'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Event Naming Convention
|
|
65
|
+
We use clear, unified event names:
|
|
66
|
+
- **Settings**: `shift_set`, `shift_set_at_time`, `timezone_set`
|
|
67
|
+
- **Events**: `event_now`, `event_now_shifted`, `event_at_time`, `event_at_time_shifted`, `event_in_timezone`, `event_in_timezone_at_time`
|
|
68
|
+
|
|
69
|
+
This naming is consistent, self-explanatory, and avoids domain-specific jargon.
|
|
70
|
+
|
|
71
|
+
## KISS Principles Applied
|
|
72
|
+
|
|
73
|
+
### 1. Simple Method Design
|
|
74
|
+
```ruby
|
|
75
|
+
# โ
Good - Single responsibility, clear intent
|
|
76
|
+
def parse_shift(shift_string)
|
|
77
|
+
return nil if shift_string.nil? || shift_string.empty?
|
|
78
|
+
|
|
79
|
+
shift = Integer(shift_string)
|
|
80
|
+
validate_shift_range(shift)
|
|
81
|
+
shift
|
|
82
|
+
rescue ArgumentError
|
|
83
|
+
raise Core::InvalidShift, shift_string
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# โ Avoid - Complex, multiple responsibilities
|
|
87
|
+
def parse_shift_and_maybe_validate_and_log(shift_string, should_log = false)
|
|
88
|
+
# ... complex logic mixing concerns
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 2. Clear Variable Naming
|
|
93
|
+
```ruby
|
|
94
|
+
# โ
Self-documenting names
|
|
95
|
+
effective_time = result.effective_time || result.message.timestamp
|
|
96
|
+
user_timezone_shift = shift_service.get_active_shift(user, timestamp)
|
|
97
|
+
|
|
98
|
+
# โ Unclear abbreviations
|
|
99
|
+
eff_t = result.eff_t || result.msg.ts
|
|
100
|
+
usr_tz_sh = shift_svc.get_act_sh(usr, ts)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 3. Prefer Simple Data Structures
|
|
104
|
+
```ruby
|
|
105
|
+
# โ
Use Ruby 3+ Data classes for immutable structs
|
|
106
|
+
ParseResult = Data.define(:type, :status, :message, :error_details)
|
|
107
|
+
|
|
108
|
+
# โ
Simple hash for configuration
|
|
109
|
+
config = {
|
|
110
|
+
default_timezone: "UTC",
|
|
111
|
+
max_shift_hours: 12
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# โ Avoid over-engineered class hierarchies for simple data
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 4. Error Handling
|
|
118
|
+
```ruby
|
|
119
|
+
# โ
Explicit error types with clear messages
|
|
120
|
+
def parse_datetime(datetime_string)
|
|
121
|
+
# Try each format
|
|
122
|
+
config.date_formats.each do |format|
|
|
123
|
+
return parse_with_format(datetime_string, format)
|
|
124
|
+
rescue ArgumentError
|
|
125
|
+
next
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
raise Core::InvalidDateTime, "Unable to parse: #{datetime_string}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# โ Generic error swallowing
|
|
132
|
+
def parse_datetime(datetime_string)
|
|
133
|
+
begin
|
|
134
|
+
# ... complex parsing
|
|
135
|
+
rescue => e
|
|
136
|
+
nil # Lost error context
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Method Organization
|
|
142
|
+
|
|
143
|
+
### Size Limits
|
|
144
|
+
- **Methods**: Max 15 lines (StandardRB default)
|
|
145
|
+
- **Classes**: Max 100 lines for clarity
|
|
146
|
+
- **Files**: Single responsibility per file
|
|
147
|
+
|
|
148
|
+
### Method Structure
|
|
149
|
+
```ruby
|
|
150
|
+
def process_toilet_shift(result)
|
|
151
|
+
user = result.message.sender
|
|
152
|
+
effective_time = determine_effective_time(result)
|
|
153
|
+
|
|
154
|
+
if retroactive_shift?(result)
|
|
155
|
+
apply_retroactive_shift(user, result.shift, effective_time)
|
|
156
|
+
else
|
|
157
|
+
apply_immediate_shift(user, result.shift, effective_time)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
result
|
|
161
|
+
rescue Core::Error => e
|
|
162
|
+
create_error_result(result, e)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def retroactive_shift?(result)
|
|
168
|
+
result.effective_time && result.effective_time < result.message.timestamp
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Testing Style
|
|
173
|
+
|
|
174
|
+
### RSpec Conventions
|
|
175
|
+
```ruby
|
|
176
|
+
# โ
Clear, descriptive test names
|
|
177
|
+
RSpec.describe MessageParser do
|
|
178
|
+
describe "#parse" do
|
|
179
|
+
context "when parsing toilet shift commands" do
|
|
180
|
+
it "creates ToiletShiftResult for valid shift" do
|
|
181
|
+
message = build_message("๐ฝ +2")
|
|
182
|
+
result = parser.parse(message)
|
|
183
|
+
|
|
184
|
+
expect(result).to be_success
|
|
185
|
+
expect(result.value.shift).to eq(2)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# โ
Use subject and let for clarity
|
|
192
|
+
subject(:parser) { described_class.new }
|
|
193
|
+
let(:message) { build_message(content) }
|
|
194
|
+
let(:content) { "๐ฝ +2" }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Test Organization
|
|
198
|
+
- One test file per class/module
|
|
199
|
+
- Group related tests with `describe` and `context`
|
|
200
|
+
- Use `let` for test data setup
|
|
201
|
+
- Keep tests focused on single behavior
|
|
202
|
+
|
|
203
|
+
## Performance Considerations
|
|
204
|
+
|
|
205
|
+
### Array and Hash Operations
|
|
206
|
+
```ruby
|
|
207
|
+
# โ
Leverage Ruby 3.4 performance improvements
|
|
208
|
+
large_array.each { process_item(it) } # 7x faster in Ruby 3.4
|
|
209
|
+
results.select { it.success? } # Optimized in Ruby 3.4
|
|
210
|
+
|
|
211
|
+
# โ
Use appropriate collection methods
|
|
212
|
+
timezone_map = timezones.to_h { [it.name, it.offset] }
|
|
213
|
+
valid_results = results.filter_map { it if it.valid? }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Regex Compilation
|
|
217
|
+
```ruby
|
|
218
|
+
# โ
Compile expensive regexes once
|
|
219
|
+
class Configuration
|
|
220
|
+
def whatsapp_message_pattern
|
|
221
|
+
@whatsapp_message_pattern ||= compile_whatsapp_pattern
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
def compile_whatsapp_pattern
|
|
227
|
+
# Complex regex compilation
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Documentation
|
|
233
|
+
|
|
234
|
+
### Inline Comments
|
|
235
|
+
```ruby
|
|
236
|
+
# โ
Explain business logic, not obvious code
|
|
237
|
+
def determine_status(event_time, message_time)
|
|
238
|
+
return :error unless event_time && message_time
|
|
239
|
+
|
|
240
|
+
# Alert if the time difference is suspiciously large (>30 days)
|
|
241
|
+
# This catches potential data entry errors
|
|
242
|
+
time_diff = (message_time - event_time).abs
|
|
243
|
+
time_diff > 30.days ? :alert : :ok
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# โ Don't comment obvious code
|
|
247
|
+
# Set user to the message sender
|
|
248
|
+
user = message.sender
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Method Documentation
|
|
252
|
+
```ruby
|
|
253
|
+
# Document complex public APIs only
|
|
254
|
+
def analyze_lines(lines)
|
|
255
|
+
# Parse all lines first
|
|
256
|
+
results = lines.filter_map { analyze_line(it) }
|
|
257
|
+
|
|
258
|
+
# Apply retroactive shifts to adjust timestamps
|
|
259
|
+
apply_retroactive_shifts_to_results(results)
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## File Organization
|
|
264
|
+
|
|
265
|
+
### Directory Structure
|
|
266
|
+
```
|
|
267
|
+
lib/
|
|
268
|
+
โโโ toilet_tracker.rb # Main entry point & API
|
|
269
|
+
โโโ toilet_tracker/
|
|
270
|
+
โ โโโ version.rb # Gem version
|
|
271
|
+
โ โโโ configuration.rb # Config & patterns
|
|
272
|
+
โ โโโ core/ # Domain objects
|
|
273
|
+
โ โ โโโ errors.rb # Custom exceptions
|
|
274
|
+
โ โ โโโ message.rb # Message value object
|
|
275
|
+
โ โ โโโ result.rb # Result monad
|
|
276
|
+
โ โ โโโ parse_result.rb # Data classes for results
|
|
277
|
+
โ โโโ parsers/ # Parsing logic
|
|
278
|
+
โ โ โโโ base_parser.rb # Shared behavior
|
|
279
|
+
โ โ โโโ whatsapp_parser.rb # WhatsApp format
|
|
280
|
+
โ โ โโโ message_parser.rb # Content parsing
|
|
281
|
+
โ โโโ services/ # Business logic
|
|
282
|
+
โ โโโ analysis_service.rb # Main orchestrator
|
|
283
|
+
โ โโโ shift_service.rb # Timezone shifts
|
|
284
|
+
โ โโโ timezone_service.rb # Timezone handling
|
|
285
|
+
|
|
286
|
+
exe/
|
|
287
|
+
โโโ toilet_tracker # Thor CLI executable
|
|
288
|
+
|
|
289
|
+
spec/
|
|
290
|
+
โโโ spec_helper.rb # Test configuration
|
|
291
|
+
โโโ toilet_tracker_spec.rb # Main API tests
|
|
292
|
+
โโโ toilet_tracker/ # Organized test structure
|
|
293
|
+
โโโ core/
|
|
294
|
+
โโโ parsers/
|
|
295
|
+
โโโ services/
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Naming Conventions
|
|
299
|
+
- **Files**: `snake_case.rb`
|
|
300
|
+
- **Classes/Modules**: `PascalCase`
|
|
301
|
+
- **Methods/Variables**: `snake_case`
|
|
302
|
+
- **Constants**: `SCREAMING_SNAKE_CASE`
|
|
303
|
+
|
|
304
|
+
## Dependencies
|
|
305
|
+
|
|
306
|
+
### Runtime Dependencies
|
|
307
|
+
```ruby
|
|
308
|
+
# Keep minimal, prefer standard library
|
|
309
|
+
gem "activesupport", "~> 8.0" # For TimeZone support only
|
|
310
|
+
gem "thor", "~> 1.3" # CLI framework
|
|
311
|
+
gem "tty-prompt", "~> 0.23" # Interactive CLI
|
|
312
|
+
gem "csv", "~> 3.3" # Ruby 3.4 compatibility
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Development Dependencies
|
|
316
|
+
```ruby
|
|
317
|
+
gem "standard" # Zero-config linting
|
|
318
|
+
gem "rspec" # Testing framework
|
|
319
|
+
gem "simplecov" # Code coverage
|
|
320
|
+
gem "rake" # Task runner
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Anti-Patterns to Avoid
|
|
324
|
+
|
|
325
|
+
### โ Over-Engineering
|
|
326
|
+
```ruby
|
|
327
|
+
# Don't create complex inheritance hierarchies for simple data
|
|
328
|
+
class BaseResult < AbstractResult
|
|
329
|
+
include ResultBehaviors
|
|
330
|
+
extend ResultClassMethods
|
|
331
|
+
# ... 50 lines of abstract methods
|
|
332
|
+
end
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### โ Magic Numbers
|
|
336
|
+
```ruby
|
|
337
|
+
# Don't use unexplained constants
|
|
338
|
+
time_diff > 2592000 ? :alert : :ok # What is 2592000?
|
|
339
|
+
|
|
340
|
+
# โ
Use named constants
|
|
341
|
+
time_diff > 30.days ? :alert : :ok
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### โ Method Chaining Abuse
|
|
345
|
+
```ruby
|
|
346
|
+
# Don't create unreadable chains
|
|
347
|
+
result.message.content.strip.downcase.gsub(/\s+/, " ").split.first&.upcase
|
|
348
|
+
|
|
349
|
+
# โ
Break into readable steps
|
|
350
|
+
content = result.message.content
|
|
351
|
+
normalized_content = normalize_whitespace(content)
|
|
352
|
+
first_word = normalized_content.split.first
|
|
353
|
+
first_word&.upcase
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Tools Integration
|
|
357
|
+
|
|
358
|
+
### Git Hooks (Overcommit)
|
|
359
|
+
```yaml
|
|
360
|
+
# .overcommit.yml
|
|
361
|
+
PreCommit:
|
|
362
|
+
StandardRb:
|
|
363
|
+
enabled: true
|
|
364
|
+
command: ['bundle', 'exec', 'standardrb']
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### CI/CD
|
|
368
|
+
```yaml
|
|
369
|
+
# GitHub Actions
|
|
370
|
+
- name: Run StandardRB
|
|
371
|
+
run: bundle exec standardrb
|
|
372
|
+
|
|
373
|
+
- name: Run Tests
|
|
374
|
+
run: bundle exec rspec
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
This style guide ensures ToiletTracker follows modern Ruby best practices while maintaining simplicity and readability.
|