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.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ToiletTracker
4
+ VERSION = '0.1.0'
5
+ 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
@@ -0,0 +1,4 @@
1
+ module ToiletTracker
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ 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: []