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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/time'
|
|
4
|
+
|
|
5
|
+
module ToiletTracker
|
|
6
|
+
module Services
|
|
7
|
+
class TimezoneService
|
|
8
|
+
attr_reader :config
|
|
9
|
+
|
|
10
|
+
def initialize(config = ToiletTracker.configuration)
|
|
11
|
+
@config = config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def calculate_shift(from_timezone, to_timezone, at_time = Time.current)
|
|
15
|
+
from_zone = resolve_timezone(from_timezone)
|
|
16
|
+
to_zone = resolve_timezone(to_timezone)
|
|
17
|
+
|
|
18
|
+
# Calculate offset difference in hours
|
|
19
|
+
from_offset = from_zone.at(at_time).utc_offset
|
|
20
|
+
to_offset = to_zone.at(at_time).utc_offset
|
|
21
|
+
|
|
22
|
+
(to_offset - from_offset) / 3600
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def apply_shift(time, shift_hours)
|
|
26
|
+
return time if shift_hours.zero?
|
|
27
|
+
|
|
28
|
+
time + shift_hours.hours
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def resolve_timezone(timezone_spec)
|
|
32
|
+
case timezone_spec
|
|
33
|
+
when String
|
|
34
|
+
resolve_timezone_string(timezone_spec)
|
|
35
|
+
when ActiveSupport::TimeZone
|
|
36
|
+
timezone_spec
|
|
37
|
+
else
|
|
38
|
+
raise Core::InvalidTimezone, timezone_spec
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def timezone_abbreviations
|
|
43
|
+
@timezone_abbreviations ||= build_abbreviation_map
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def resolve_timezone_string(timezone_str)
|
|
49
|
+
# First try as a named timezone (e.g., "Europe/Rome", "UTC")
|
|
50
|
+
zone = ActiveSupport::TimeZone[timezone_str]
|
|
51
|
+
return zone if zone
|
|
52
|
+
|
|
53
|
+
# Try as an abbreviation (e.g., "PST", "EST")
|
|
54
|
+
abbreviation_zones = timezone_abbreviations[timezone_str.upcase]
|
|
55
|
+
if abbreviation_zones&.any?
|
|
56
|
+
# If multiple zones have the same abbreviation, use the first one
|
|
57
|
+
# In a real application, you might want to be more sophisticated here
|
|
58
|
+
return abbreviation_zones.first
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Try to parse as offset (e.g., "UTC+5", "GMT-3")
|
|
62
|
+
return ActiveSupport::TimeZone[timezone_str] if timezone_str.match?(/^(UTC|GMT)([+-]\d{1,2})$/)
|
|
63
|
+
|
|
64
|
+
raise Core::InvalidTimezone, timezone_str
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_abbreviation_map
|
|
68
|
+
map = Hash.new { |h, k| h[k] = [] }
|
|
69
|
+
|
|
70
|
+
ActiveSupport::TimeZone.all.each do |zone|
|
|
71
|
+
collect_zone_abbreviations(zone, map)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
map
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def collect_zone_abbreviations(zone, map)
|
|
78
|
+
sample_times.each do |time|
|
|
79
|
+
abbr = zone.tzinfo.abbreviation(time).to_s.upcase
|
|
80
|
+
add_zone_to_map(map, abbr, zone)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def sample_times
|
|
85
|
+
[Time.current, Time.current + 6.months]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_zone_to_map(map, abbr, zone)
|
|
89
|
+
map[abbr] << zone unless map[abbr].include?(zone)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'zip'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
module ToiletTracker
|
|
7
|
+
module Utils
|
|
8
|
+
class ZipHandler
|
|
9
|
+
class ZipError < StandardError; end
|
|
10
|
+
|
|
11
|
+
class ChatFileNotFoundError < ZipError; end
|
|
12
|
+
|
|
13
|
+
def self.extract_chat_file(zip_path)
|
|
14
|
+
new(zip_path).extract_chat_file
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.zip_file?(file_path)
|
|
18
|
+
File.extname(file_path).downcase == '.zip'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(zip_path)
|
|
22
|
+
@zip_path = zip_path
|
|
23
|
+
validate_zip_file!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def extract_chat_file
|
|
27
|
+
chat_entries = find_chat_entries
|
|
28
|
+
|
|
29
|
+
raise ChatFileNotFoundError, 'No _chat.txt file found in zip archive' if chat_entries.empty?
|
|
30
|
+
|
|
31
|
+
chat_entry = if chat_entries.size > 1
|
|
32
|
+
# If multiple _chat.txt files, prefer the one in root or with shortest path
|
|
33
|
+
chat_entries.min_by { |entry| entry.name.count('/') }
|
|
34
|
+
else
|
|
35
|
+
chat_entries.first
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
extract_to_tempfile(chat_entry)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def validate_zip_file!
|
|
44
|
+
raise ZipError, "Zip file not found: #{@zip_path}" unless File.exist?(@zip_path)
|
|
45
|
+
|
|
46
|
+
return if File.readable?(@zip_path)
|
|
47
|
+
|
|
48
|
+
raise ZipError, "Zip file not readable: #{@zip_path}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def find_chat_entries
|
|
52
|
+
chat_entries = []
|
|
53
|
+
|
|
54
|
+
Zip::File.open(@zip_path) do |zip|
|
|
55
|
+
zip.each do |entry|
|
|
56
|
+
# Look for files named exactly _chat.txt (case insensitive)
|
|
57
|
+
chat_entries << entry if File.basename(entry.name).match?(/^_chat\.txt$/i) && !entry.directory?
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
chat_entries
|
|
62
|
+
rescue Zip::Error => e
|
|
63
|
+
raise ZipError, "Invalid zip file: #{e.message}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def extract_to_tempfile(entry)
|
|
67
|
+
temp_file = Tempfile.new(['chat_export', '.txt'])
|
|
68
|
+
|
|
69
|
+
begin
|
|
70
|
+
Zip::File.open(@zip_path) do |_zip|
|
|
71
|
+
content = entry.get_input_stream.read
|
|
72
|
+
temp_file.write(content)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
temp_file.close
|
|
76
|
+
temp_file.path
|
|
77
|
+
rescue Zip::Error => e
|
|
78
|
+
temp_file&.close
|
|
79
|
+
temp_file&.unlink
|
|
80
|
+
raise ZipError, "Failed to extract chat file: #{e.message}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/time'
|
|
4
|
+
|
|
5
|
+
require_relative 'toilet_tracker/version'
|
|
6
|
+
require_relative 'toilet_tracker/configuration'
|
|
7
|
+
require_relative 'toilet_tracker/core/errors'
|
|
8
|
+
require_relative 'toilet_tracker/core/message'
|
|
9
|
+
require_relative 'toilet_tracker/core/result'
|
|
10
|
+
require_relative 'toilet_tracker/core/parse_result'
|
|
11
|
+
require_relative 'toilet_tracker/parsers/base_parser'
|
|
12
|
+
require_relative 'toilet_tracker/parsers/whatsapp_parser'
|
|
13
|
+
require_relative 'toilet_tracker/parsers/message_parser'
|
|
14
|
+
require_relative 'toilet_tracker/services/timezone_service'
|
|
15
|
+
require_relative 'toilet_tracker/services/shift_service'
|
|
16
|
+
require_relative 'toilet_tracker/services/analysis_service'
|
|
17
|
+
require_relative 'toilet_tracker/utils/zip_handler'
|
|
18
|
+
|
|
19
|
+
module ToiletTracker
|
|
20
|
+
class Error < StandardError; end
|
|
21
|
+
|
|
22
|
+
def self.parse_line(line)
|
|
23
|
+
analysis_service = Services::AnalysisService.new
|
|
24
|
+
analysis_service.analyze_line(line)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.parse_lines(lines)
|
|
28
|
+
analysis_service = Services::AnalysisService.new
|
|
29
|
+
analysis_service.analyze_lines(lines)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.reset!
|
|
33
|
+
# Reset configuration to defaults
|
|
34
|
+
reset_configuration!
|
|
35
|
+
end
|
|
36
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: toilet_tracker
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- NetherSoul
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 8.0.2.1
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 8.0.2.1
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: csv
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 3.3.5
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 3.3.5
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rubyzip
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 3.0.1
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 3.0.1
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: thor
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 1.4.0
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 1.4.0
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: tty-prompt
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 0.23.1
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 0.23.1
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: zeitwerk
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 2.7.3
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 2.7.3
|
|
96
|
+
description: "A Ruby gem that parses WhatsApp messages containing toilet tracking
|
|
97
|
+
data using emoji commands (\U0001F6BD for settings, \U0001F4A9 for events) with
|
|
98
|
+
timezone support\n"
|
|
99
|
+
email:
|
|
100
|
+
- nethersoul.dev@gmail.com
|
|
101
|
+
executables:
|
|
102
|
+
- toilet_tracker
|
|
103
|
+
extensions: []
|
|
104
|
+
extra_rdoc_files: []
|
|
105
|
+
files:
|
|
106
|
+
- ".overcommit.yml"
|
|
107
|
+
- ".rspec"
|
|
108
|
+
- ".rubocop.yml"
|
|
109
|
+
- ".standard.yml"
|
|
110
|
+
- CHANGELOG.md
|
|
111
|
+
- CODE_OF_CONDUCT.md
|
|
112
|
+
- LICENSE.txt
|
|
113
|
+
- PARSING_SPEC.md
|
|
114
|
+
- README.md
|
|
115
|
+
- Rakefile
|
|
116
|
+
- STYLE_GUIDE.md
|
|
117
|
+
- exe/toilet_tracker
|
|
118
|
+
- lib/toilet_tracker.rb
|
|
119
|
+
- lib/toilet_tracker/configuration.rb
|
|
120
|
+
- lib/toilet_tracker/core/errors.rb
|
|
121
|
+
- lib/toilet_tracker/core/message.rb
|
|
122
|
+
- lib/toilet_tracker/core/parse_result.rb
|
|
123
|
+
- lib/toilet_tracker/core/result.rb
|
|
124
|
+
- lib/toilet_tracker/parsers/base_parser.rb
|
|
125
|
+
- lib/toilet_tracker/parsers/message_parser.rb
|
|
126
|
+
- lib/toilet_tracker/parsers/whatsapp_parser.rb
|
|
127
|
+
- lib/toilet_tracker/services/analysis_service.rb
|
|
128
|
+
- lib/toilet_tracker/services/shift_service.rb
|
|
129
|
+
- lib/toilet_tracker/services/timezone_service.rb
|
|
130
|
+
- lib/toilet_tracker/utils/zip_handler.rb
|
|
131
|
+
- lib/toilet_tracker/version.rb
|
|
132
|
+
- sig/toilet_tracker.rbs
|
|
133
|
+
homepage: https://github.com/brownops/toilet_tracker
|
|
134
|
+
licenses:
|
|
135
|
+
- MIT
|
|
136
|
+
metadata:
|
|
137
|
+
allowed_push_host: https://rubygems.org
|
|
138
|
+
homepage_uri: https://github.com/brownops/toilet_tracker
|
|
139
|
+
source_code_uri: https://github.com/brownops/toilet_tracker
|
|
140
|
+
changelog_uri: https://github.com/brownops/toilet_tracker/blob/main/CHANGELOG.md
|
|
141
|
+
rubygems_mfa_required: 'true'
|
|
142
|
+
rdoc_options: []
|
|
143
|
+
require_paths:
|
|
144
|
+
- lib
|
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
146
|
+
requirements:
|
|
147
|
+
- - ">="
|
|
148
|
+
- !ruby/object:Gem::Version
|
|
149
|
+
version: 3.4.0
|
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
|
+
requirements:
|
|
152
|
+
- - ">="
|
|
153
|
+
- !ruby/object:Gem::Version
|
|
154
|
+
version: '0'
|
|
155
|
+
requirements: []
|
|
156
|
+
rubygems_version: 3.6.7
|
|
157
|
+
specification_version: 4
|
|
158
|
+
summary: Parse WhatsApp messages for toilet tracking with emoji-based commands
|
|
159
|
+
test_files: []
|