hizuke 0.0.2 → 0.0.4

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: 064d7574f7151fdf7695b789dd4812f38c6becb6226b7bda807d0339dc0fc8e1
4
- data.tar.gz: 48c23cc779ae564bc3625176f7af74ebb2c0b33a7f4f67cea5098851c35de2e0
3
+ metadata.gz: 6a44032e09036e25847a067394cf50b60f47197e5fe4d29b068c0e822960db9d
4
+ data.tar.gz: 8f6a2d515fe0b1b64e970ef5ced8aad5aaa96febfd6c44448dc765d78521a709
5
5
  SHA512:
6
- metadata.gz: 22380b935099c93601a9ac54837e4378935037d653e5ee557c1fc7cdd31098245fbb3bc4c3e5c6623c431fdb9f54138fb3445dbe18e9197721caf6d082c25e16
7
- data.tar.gz: e00e4fe32d48188761db0f070d55bddf9bd31915f2a4336c2846704aaf4286f1b1e7c132070029a39afa558a264b1badc1734934c47f9d059334a9f06d809416
6
+ metadata.gz: be87cac6f8fdf4ca69ca42641ad214804787a498b019d80b80b263835ce5fed52215af0d1f6e04e30e12889c710cb56db7df19a2c20c056c4969c97cfddbbd90
7
+ data.tar.gz: 8e36da5a76ea3f9940b3970bab64a6dd2fa33a050caf02d9dd743e2052773ea4f28678f142dc82ce4b16dd436d0396b07afcb8bf961f9168e5d9d8fed2c733e6
data/CHANGELOG.md CHANGED
@@ -5,6 +5,57 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.0.4] - 2025-04-29
9
+
10
+ ### Added
11
+ - Support for time recognition in text:
12
+ - "at X" format (e.g., "at 10" for 10:00)
13
+ - "@ X" alternative syntax
14
+ - Time with minutes "at X:Y" (e.g., "at 10:30")
15
+ - Time with seconds "at X:Y:Z" (e.g., "at 10:30:45")
16
+ - AM/PM format "at Xam/pm" (e.g., "at 10am", "at 7pm")
17
+ - New attributes in Result class:
18
+ - `time` attribute to access the extracted time
19
+ - `datetime` method to get a combined Time object of date and time
20
+ - Created a dedicated `TimeOfDay` class for better time handling with cleaner display
21
+ - Support for word-based time expressions:
22
+ - "at noon" - returns 12:00
23
+ - "at midnight" - returns 00:00
24
+ - "in the morning" - returns configurable time (default 08:00)
25
+ - "in the evening" - returns configurable time (default 20:00)
26
+ - Configuration system to customize times for "morning" and "evening"
27
+ - Comprehensive tests for time parsing functionality
28
+ - Updated documentation with time parsing examples and supported formats
29
+
30
+ ## [0.0.3] - 2025-04-24
31
+
32
+ ### Added
33
+ - Support for quarterly date references:
34
+ - "next quarter" / "nextquarter" - returns the first day of the next quarter
35
+ - "last quarter" / "lastquarter" - returns the first day of the last quarter
36
+ - Support for relative day references:
37
+ - "day after tomorrow" / "dayaftertomorrow"
38
+ - "day before yesterday" / "daybeforeyesterday"
39
+ - Support for dynamic time spans:
40
+ - "in X days", "X days ago"
41
+ - "in X weeks", "X weeks ago"
42
+ - "in X months", "X months ago"
43
+ - "in X years", "X years ago"
44
+ - Support for specific days of the week:
45
+ - "this Monday", "this Tuesday", etc.
46
+ - "next Monday", "next Tuesday", etc.
47
+ - "last Monday", "last Tuesday", etc.
48
+ - Support for additional time references:
49
+ - "last week" / "lastweek"
50
+ - "last month" / "lastmonth"
51
+ - "last year" / "lastyear"
52
+ - "end of week" / "endofweek"
53
+ - "end of month" / "endofmonth"
54
+ - "end of year" / "endofyear"
55
+ - "mid week" / "midweek"
56
+ - "mid month" / "midmonth"
57
+ - Updated documentation to reflect all new functionality
58
+
8
59
  ## [0.0.2] - 2025-04-23
9
60
 
10
61
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hizuke (0.0.1)
4
+ hizuke (0.0.4)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Hizuke
2
2
 
3
- Hizuke is a simple Ruby gem that parses text containing date references like "yesterday", "today", and "tomorrow". It extracts the date and returns the clean text without the date reference.
3
+ Hizuke is a simple Ruby gem that parses text containing date references like "yesterday", "today", and "tomorrow". It extracts the date and returns the clean text without the date reference. It can also recognize time references like "at 10" or "at 7pm".
4
4
 
5
5
  ## Installation
6
6
 
@@ -61,10 +61,76 @@ puts result.date # => <Date: 2024-01-01> (represents the first day of the next
61
61
  result = Hizuke.parse("hiking this weekend")
62
62
  puts result.text # => "hiking"
63
63
  puts result.date # => <Date: 2023-04-01> (represents the next Saturday)
64
+
65
+ # Parse text with time
66
+ result = Hizuke.parse("meeting tomorrow at 10")
67
+ puts result.text # => "meeting"
68
+ puts result.date # => <Date: 2023-04-01> (represents tomorrow's date)
69
+ puts result.time # => 10:00 (represents the time)
70
+ puts result.datetime # => 2023-04-01 10:00:00 (combines date and time)
71
+
72
+ # Parse text with time including minutes
73
+ result = Hizuke.parse("call client today at 14:30")
74
+ puts result.text # => "call client"
75
+ puts result.date # => <Date: 2023-03-31> (represents today's date)
76
+ puts result.time # => 14:30 (represents the time)
77
+ puts result.datetime # => 2023-03-31 14:30:00 (combines date and time)
78
+
79
+ # Parse text with AM/PM time
80
+ result = Hizuke.parse("lunch meeting tomorrow at 12pm")
81
+ puts result.text # => "lunch meeting"
82
+ puts result.date # => <Date: 2023-04-01> (represents tomorrow's date)
83
+ puts result.time # => 12:00 (represents the time, noon)
84
+ puts result.datetime # => 2023-04-01 12:00:00 (combines date and time)
85
+
86
+ # Parse text with word-based time
87
+ result = Hizuke.parse("dinner tomorrow at noon")
88
+ puts result.text # => "dinner"
89
+ puts result.date # => <Date: 2023-04-01> (represents tomorrow's date)
90
+ puts result.time # => 12:00 (represents noon)
91
+
92
+ result = Hizuke.parse("flight today at midnight")
93
+ puts result.text # => "flight"
94
+ puts result.date # => <Date: 2023-03-31> (represents today's date)
95
+ puts result.time # => 00:00 (represents midnight)
96
+
97
+ result = Hizuke.parse("breakfast tomorrow in the morning")
98
+ puts result.text # => "breakfast"
99
+ puts result.date # => <Date: 2023-04-01> (represents tomorrow's date)
100
+ puts result.time # => 08:00 (default morning time)
101
+
102
+ result = Hizuke.parse("dinner today in the evening")
103
+ puts result.text # => "dinner"
104
+ puts result.date # => <Date: 2023-03-31> (represents today's date)
105
+ puts result.time # => 20:00 (default evening time)
64
106
  ```
65
107
 
66
108
  The parser is case-insensitive and can handle date references located anywhere in the text. It also supports date references with or without spaces (e.g., "nextweek" or "next week").
67
109
 
110
+ ## Configuration
111
+
112
+ You can configure the time values for "in the morning" and "in the evening" expressions:
113
+
114
+ ```ruby
115
+ Hizuke.configure do |config|
116
+ # Set morning time to 9:30
117
+ config.morning_time = { hour: 9, min: 30 }
118
+
119
+ # Set evening time to 7:00 PM
120
+ config.evening_time = { hour: 19, min: 0 }
121
+ end
122
+
123
+ # Now when parsing "in the morning", it will return 9:30
124
+ result = Hizuke.parse("breakfast tomorrow in the morning")
125
+ puts result.time # => 09:30
126
+
127
+ # And "in the evening" will return 19:00
128
+ result = Hizuke.parse("dinner today in the evening")
129
+ puts result.time # => 19:00
130
+ ```
131
+
132
+ By default, "in the morning" is set to 8:00 and "in the evening" is set to 20:00.
133
+
68
134
  ## Supported Date Keywords
69
135
 
70
136
  Currently, the following English date keywords are supported:
@@ -72,10 +138,53 @@ Currently, the following English date keywords are supported:
72
138
  - `yesterday` - returns yesterday's date
73
139
  - `today` - returns today's date
74
140
  - `tomorrow` - returns tomorrow's date
141
+ - `day after tomorrow` / `dayaftertomorrow` - returns the date two days from today
142
+ - `day before yesterday` / `daybeforeyesterday` - returns the date two days before today
143
+ - `in X days` - returns the date X days from today (where X is any number)
144
+ - `X days ago` - returns the date X days before today (where X is any number)
145
+ - `in X weeks` - returns the date X weeks from today (where X is any number)
146
+ - `X weeks ago` - returns the date X weeks before today (where X is any number)
147
+ - `in X months` - returns the date X months from today (where X is any number)
148
+ - `X months ago` - returns the date X months before today (where X is any number)
149
+ - `in X years` - returns the date X years from today (where X is any number)
150
+ - `X years ago` - returns the date X years before today (where X is any number)
151
+ - `this Monday`, `this Tuesday`, etc. - returns the date of the specified day in the current week
152
+ - `next Monday`, `next Tuesday`, etc. - returns the date of the specified day in the next week
153
+ - `last Monday`, `last Tuesday`, etc. - returns the date of the specified day in the previous week
75
154
  - `next week` / `nextweek` - returns the date of the next Monday
155
+ - `last week` / `lastweek` - returns the date of the previous Monday
76
156
  - `next month` / `nextmonth` - returns the first day of the next month
157
+ - `last month` / `lastmonth` - returns the first day of the previous month
77
158
  - `next year` / `nextyear` - returns the first day of the next year
159
+ - `last year` / `lastyear` - returns the first day of the previous year
78
160
  - `this weekend` / `thisweekend` - returns the date of the upcoming Saturday (or today if it's already the weekend)
161
+ - `end of week` / `endofweek` - returns the date of the upcoming Sunday (end of week)
162
+ - `end of month` / `endofmonth` - returns the date of the last day of the current month
163
+ - `end of year` / `endofyear` - returns the date of December 31st of the current year
164
+ - `mid week` / `midweek` - returns the date of Wednesday of the current week
165
+ - `mid month` / `midmonth` - returns the date of the 15th day of the current month
166
+ - `next quarter` / `nextquarter` - returns the first day of the next quarter
167
+ - `last quarter` / `lastquarter` - returns the first day of the last quarter
168
+
169
+ ## Supported Time Formats
170
+
171
+ The following time formats are supported:
172
+
173
+ ### Numeric time formats
174
+ - `at X` - where X is a number (e.g., "at 10" for 10:00)
175
+ - `@ X` - alternative syntax with @ symbol
176
+ - `at X:Y` - where X is hours and Y is minutes (e.g., "at 10:30")
177
+ - `at X:Y:Z` - where X is hours, Y is minutes, and Z is seconds
178
+ - `at Xam/pm` - with AM/PM indicator (e.g., "at 10am", "at 7pm")
179
+ - `at X:Yam/pm` - with minutes and AM/PM indicator (e.g., "at 10:30am")
180
+
181
+ ### Word-based time formats
182
+ - `at noon` - returns 12:00
183
+ - `at midnight` - returns 00:00
184
+ - `in the morning` - returns configurable time (default 08:00)
185
+ - `in the evening` - returns configurable time (default 20:00)
186
+
187
+ When time is included, you can access it through the `time` attribute of the result. The time is displayed in the format "HH:MM" or "HH:MM:SS" if seconds are present. Additionally, you can use the `datetime` attribute to get a Time object combining both the date and time information.
79
188
 
80
189
  ## Development
81
190
 
@@ -85,7 +194,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
85
194
 
86
195
  ## Contributing
87
196
 
88
- Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/hizuke.
197
+ Bug reports and pull requests are welcome on GitHub at https://github.com/majur/hizuke.
89
198
 
90
199
  ## License
91
200
 
data/hizuke.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/hizuke/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "hizuke"
7
+ spec.version = Hizuke::VERSION
8
+ spec.authors = ["Juraj Maťaše"]
9
+ spec.email = ["juraj@hey.com"]
10
+
11
+ spec.summary = "A simple date parser for natural language time references"
12
+ spec.description = "Hizuke is a lightweight utility that extracts dates from text by recognizing common time expressions. It cleans the original text and returns both the parsed date and the text without the date reference."
13
+ spec.homepage = "https://github.com/majur/hizuke"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.6.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Add development dependencies here
33
+ spec.add_development_dependency "minitest", "~> 5.0"
34
+ spec.add_development_dependency "rake", "~> 13.0"
35
+ spec.add_development_dependency "rubocop", "~> 1.21"
36
+ end
data/lib/hizuke/parser.rb CHANGED
@@ -1,35 +1,148 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "date"
4
+ require "time"
4
5
 
5
6
  module Hizuke
6
- # Result object containing the clean text and extracted date
7
+ # Simple class to represent a time of day without a date
8
+ class TimeOfDay
9
+ attr_reader :hour, :min, :sec
10
+
11
+ def initialize(hour, min = 0, sec = 0)
12
+ @hour = hour
13
+ @min = min
14
+ @sec = sec
15
+ end
16
+
17
+ def to_s
18
+ if sec == 0
19
+ format("%02d:%02d", hour, min)
20
+ else
21
+ format("%02d:%02d:%02d", hour, min, sec)
22
+ end
23
+ end
24
+
25
+ def inspect
26
+ to_s
27
+ end
28
+ end
29
+
30
+ # Result object containing the clean text and extracted date/time
7
31
  class Result
8
- attr_reader :text, :date
32
+ attr_reader :text, :date, :time
9
33
 
10
- def initialize(text, date)
34
+ def initialize(text, date, time = nil)
11
35
  @text = text
12
36
  @date = date
37
+ @time = time
38
+ end
39
+
40
+ def datetime
41
+ return nil unless @time
42
+
43
+ # Combine date and time into a Time object
44
+ Time.new(@date.year, @date.month, @date.day,
45
+ @time.hour, @time.min, @time.sec)
46
+ end
47
+ end
48
+
49
+ # Configuration class for Hizuke
50
+ class Configuration
51
+ attr_accessor :morning_time, :evening_time
52
+
53
+ def initialize
54
+ @morning_time = { hour: 8, min: 0 }
55
+ @evening_time = { hour: 20, min: 0 }
13
56
  end
14
57
  end
58
+
59
+ # Allows configuration of Hizuke
60
+ def self.configure
61
+ @configuration ||= Configuration.new
62
+ yield(@configuration) if block_given?
63
+ end
64
+
65
+ # Returns the configuration
66
+ def self.configuration
67
+ @configuration ||= Configuration.new
68
+ end
15
69
 
16
70
  # Parser class responsible for extracting dates from text
17
71
  class Parser
72
+ # Mapping of day names to their wday values (0-6, Sunday is 0)
73
+ DAYS_OF_WEEK = {
74
+ "monday" => 1,
75
+ "tuesday" => 2,
76
+ "wednesday" => 3,
77
+ "thursday" => 4,
78
+ "friday" => 5,
79
+ "saturday" => 6,
80
+ "sunday" => 0
81
+ }.freeze
82
+
18
83
  # Date keywords mapping
19
84
  DATE_KEYWORDS = {
20
85
  "yesterday" => -1,
21
86
  "today" => 0,
22
87
  "tomorrow" => 1,
88
+ "dayaftertomorrow" => 2,
89
+ "day after tomorrow" => 2,
90
+ "daybeforeyesterday" => -2,
91
+ "day before yesterday" => -2,
23
92
  "nextweek" => :next_week,
24
93
  "next week" => :next_week,
94
+ "lastweek" => :last_week,
95
+ "last week" => :last_week,
25
96
  "nextmonth" => :next_month,
26
97
  "next month" => :next_month,
98
+ "lastmonth" => :last_month,
99
+ "last month" => :last_month,
27
100
  "nextyear" => :next_year,
28
101
  "next year" => :next_year,
102
+ "lastyear" => :last_year,
103
+ "last year" => :last_year,
104
+ "nextquarter" => :next_quarter,
105
+ "next quarter" => :next_quarter,
106
+ "lastquarter" => :last_quarter,
107
+ "last quarter" => :last_quarter,
29
108
  "thisweekend" => :this_weekend,
30
- "this weekend" => :this_weekend
109
+ "this weekend" => :this_weekend,
110
+ "endofweek" => :end_of_week,
111
+ "end of week" => :end_of_week,
112
+ "endofmonth" => :end_of_month,
113
+ "end of month" => :end_of_month,
114
+ "endofyear" => :end_of_year,
115
+ "end of year" => :end_of_year,
116
+ "midweek" => :mid_week,
117
+ "mid week" => :mid_week,
118
+ "midmonth" => :mid_month,
119
+ "mid month" => :mid_month
31
120
  }.freeze
32
121
 
122
+ # Regex patterns for dynamic date references
123
+ IN_X_DAYS_PATTERN = /in (\d+) days?/i
124
+ X_DAYS_AGO_PATTERN = /(\d+) days? ago/i
125
+ IN_X_WEEKS_PATTERN = /in (\d+) weeks?/i
126
+ X_WEEKS_AGO_PATTERN = /(\d+) weeks? ago/i
127
+ IN_X_MONTHS_PATTERN = /in (\d+) months?/i
128
+ X_MONTHS_AGO_PATTERN = /(\d+) months? ago/i
129
+ IN_X_YEARS_PATTERN = /in (\d+) years?/i
130
+ X_YEARS_AGO_PATTERN = /(\d+) years? ago/i
131
+
132
+ # Regex patterns for specific days of the week
133
+ THIS_DAY_PATTERN = /this (monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i
134
+ NEXT_DAY_PATTERN = /next (monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i
135
+ LAST_DAY_PATTERN = /last (monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i
136
+
137
+ # Regex patterns for time references
138
+ TIME_PATTERN = /(?:at|@)\s*(\d{1,2})(?::(\d{1,2}))?(?::(\d{1,2}))?\s*(am|pm)?/i
139
+
140
+ # Regex patterns for word-based time references
141
+ NOON_PATTERN = /at\s+noon/i
142
+ MIDNIGHT_PATTERN = /at\s+midnight/i
143
+ MORNING_PATTERN = /in\s+the\s+morning/i
144
+ EVENING_PATTERN = /in\s+the\s+evening/i
145
+
33
146
  # Parse text containing time references and extract both
34
147
  # the clean text and the date.
35
148
  #
@@ -49,12 +162,62 @@ module Hizuke
49
162
  # Check if text is nil or empty
50
163
  raise ParseError, "Input text cannot be nil or empty" if text.nil? || text.empty?
51
164
 
165
+ # Extract time if present
166
+ extracted_time = nil
167
+ clean_text = text
168
+
169
+ # Try to match word-based time patterns first
170
+ if match = clean_text.match(NOON_PATTERN)
171
+ extracted_time = TimeOfDay.new(12, 0, 0)
172
+ clean_text = clean_text.gsub(match[0], "").strip
173
+ elsif match = clean_text.match(MIDNIGHT_PATTERN)
174
+ extracted_time = TimeOfDay.new(0, 0, 0)
175
+ clean_text = clean_text.gsub(match[0], "").strip
176
+ elsif match = clean_text.match(MORNING_PATTERN)
177
+ config = Hizuke.configuration
178
+ extracted_time = TimeOfDay.new(config.morning_time[:hour], config.morning_time[:min], 0)
179
+ clean_text = clean_text.gsub(match[0], "").strip
180
+ elsif match = clean_text.match(EVENING_PATTERN)
181
+ config = Hizuke.configuration
182
+ extracted_time = TimeOfDay.new(config.evening_time[:hour], config.evening_time[:min], 0)
183
+ clean_text = clean_text.gsub(match[0], "").strip
184
+ # Then try the numeric time pattern
185
+ elsif time_match = clean_text.match(TIME_PATTERN)
186
+ hour = time_match[1].to_i
187
+ min = time_match[2] ? time_match[2].to_i : 0
188
+ sec = time_match[3] ? time_match[3].to_i : 0
189
+
190
+ # Adjust for AM/PM
191
+ if time_match[4]&.downcase == "pm" && hour < 12
192
+ hour += 12
193
+ elsif time_match[4]&.downcase == "am" && hour == 12
194
+ hour = 0
195
+ end
196
+
197
+ extracted_time = TimeOfDay.new(hour, min, sec)
198
+
199
+ # Remove the time expression from the text
200
+ clean_text = clean_text.gsub(time_match[0], "").strip
201
+ end
202
+
203
+ # Check for dynamic patterns first (in X days, X days ago)
204
+ result = check_dynamic_patterns(clean_text)
205
+ if result
206
+ return Result.new(result.text, result.date, extracted_time)
207
+ end
208
+
209
+ # Check for day of week patterns (this Monday, next Tuesday, etc.)
210
+ result = check_day_of_week_patterns(clean_text)
211
+ if result
212
+ return Result.new(result.text, result.date, extracted_time)
213
+ end
214
+
52
215
  # Try to find compound date expressions (like "next week")
53
216
  compound_matches = {}
54
217
 
55
218
  DATE_KEYWORDS.keys.select { |k| k.include?(" ") }.each do |compound_key|
56
- if text.downcase.include?(compound_key)
57
- start_idx = text.downcase.index(compound_key)
219
+ if clean_text.downcase.include?(compound_key)
220
+ start_idx = clean_text.downcase.index(compound_key)
58
221
  end_idx = start_idx + compound_key.length - 1
59
222
  compound_matches[compound_key] = [start_idx, end_idx]
60
223
  end
@@ -70,15 +233,15 @@ module Hizuke
70
233
  date = calculate_date(date_value)
71
234
 
72
235
  # Remove the date expression from the text
73
- clean_text = text.dup
74
- clean_text.slice!(indices[0]..indices[1])
75
- clean_text = clean_text.strip
236
+ final_text = clean_text.dup
237
+ final_text.slice!(indices[0]..indices[1])
238
+ final_text = final_text.strip
76
239
 
77
- return Result.new(clean_text, date)
240
+ return Result.new(final_text, date, extracted_time)
78
241
  end
79
242
 
80
243
  # Split the text into words (for single-word date references)
81
- words = text.split
244
+ words = clean_text.split
82
245
 
83
246
  # Find the first date keyword
84
247
  date_word_index = nil
@@ -94,7 +257,7 @@ module Hizuke
94
257
  end
95
258
 
96
259
  if date_word_index.nil?
97
- raise ParseError, "No valid date reference found in '#{text}'"
260
+ raise ParseError, "No valid date reference found in '#{clean_text}'"
98
261
  end
99
262
 
100
263
  # Calculate the date based on the keyword
@@ -103,13 +266,165 @@ module Hizuke
103
266
  # Create the clean text by removing the date keyword
104
267
  clean_words = words.dup
105
268
  clean_words.delete_at(date_word_index)
106
- clean_text = clean_words.join(" ").strip
269
+ final_text = clean_words.join(" ").strip
107
270
 
108
- Result.new(clean_text, date)
271
+ Result.new(final_text, date, extracted_time)
109
272
  end
110
273
 
111
274
  private
112
275
 
276
+ # Check for day of week patterns (this Monday, next Tuesday, last Friday, etc.)
277
+ def check_day_of_week_patterns(text)
278
+ # Check for "this [day]" pattern
279
+ if (match = text.match(THIS_DAY_PATTERN))
280
+ day_name = match[1].downcase
281
+ day_value = DAYS_OF_WEEK[day_name]
282
+ date = calculate_this_day(day_value)
283
+ clean_text = text.gsub(match[0], "").strip
284
+ return Result.new(clean_text, date)
285
+ end
286
+
287
+ # Check for "next [day]" pattern
288
+ if (match = text.match(NEXT_DAY_PATTERN))
289
+ day_name = match[1].downcase
290
+ day_value = DAYS_OF_WEEK[day_name]
291
+ date = calculate_next_day(day_value)
292
+ clean_text = text.gsub(match[0], "").strip
293
+ return Result.new(clean_text, date)
294
+ end
295
+
296
+ # Check for "last [day]" pattern
297
+ if (match = text.match(LAST_DAY_PATTERN))
298
+ day_name = match[1].downcase
299
+ day_value = DAYS_OF_WEEK[day_name]
300
+ date = calculate_last_day(day_value)
301
+ clean_text = text.gsub(match[0], "").strip
302
+ return Result.new(clean_text, date)
303
+ end
304
+
305
+ nil
306
+ end
307
+
308
+ # Check for dynamic date patterns like "in X days" or "X days ago"
309
+ def check_dynamic_patterns(text)
310
+ # Check for "in X days" pattern
311
+ if (match = text.match(IN_X_DAYS_PATTERN))
312
+ days = match[1].to_i
313
+ date = Date.today + days
314
+ clean_text = text.gsub(match[0], "").strip
315
+ return Result.new(clean_text, date)
316
+ end
317
+
318
+ # Check for "X days ago" pattern
319
+ if (match = text.match(X_DAYS_AGO_PATTERN))
320
+ days = match[1].to_i
321
+ date = Date.today - days
322
+ clean_text = text.gsub(match[0], "").strip
323
+ return Result.new(clean_text, date)
324
+ end
325
+
326
+ # Check for "in X weeks" pattern
327
+ if (match = text.match(IN_X_WEEKS_PATTERN))
328
+ weeks = match[1].to_i
329
+ date = Date.today + (weeks * 7)
330
+ clean_text = text.gsub(match[0], "").strip
331
+ return Result.new(clean_text, date)
332
+ end
333
+
334
+ # Check for "X weeks ago" pattern
335
+ if (match = text.match(X_WEEKS_AGO_PATTERN))
336
+ weeks = match[1].to_i
337
+ date = Date.today - (weeks * 7)
338
+ clean_text = text.gsub(match[0], "").strip
339
+ return Result.new(clean_text, date)
340
+ end
341
+
342
+ # Check for "in X months" pattern
343
+ if (match = text.match(IN_X_MONTHS_PATTERN))
344
+ months = match[1].to_i
345
+ date = Date.today >> months
346
+ clean_text = text.gsub(match[0], "").strip
347
+ return Result.new(clean_text, date)
348
+ end
349
+
350
+ # Check for "X months ago" pattern
351
+ if (match = text.match(X_MONTHS_AGO_PATTERN))
352
+ months = match[1].to_i
353
+ date = Date.today << months
354
+ clean_text = text.gsub(match[0], "").strip
355
+ return Result.new(clean_text, date)
356
+ end
357
+
358
+ # Check for "in X years" pattern
359
+ if (match = text.match(IN_X_YEARS_PATTERN))
360
+ years = match[1].to_i
361
+ date = Date.new(Date.today.year + years, Date.today.month, Date.today.day)
362
+ clean_text = text.gsub(match[0], "").strip
363
+ return Result.new(clean_text, date)
364
+ end
365
+
366
+ # Check for "X years ago" pattern
367
+ if (match = text.match(X_YEARS_AGO_PATTERN))
368
+ years = match[1].to_i
369
+ date = Date.new(Date.today.year - years, Date.today.month, Date.today.day)
370
+ clean_text = text.gsub(match[0], "").strip
371
+ return Result.new(clean_text, date)
372
+ end
373
+
374
+ nil
375
+ end
376
+
377
+ # Calculate date for "this [day]" - the current/upcoming day in this week
378
+ def calculate_this_day(target_wday)
379
+ today = Date.today
380
+ today_wday = today.wday
381
+
382
+ # Calculate days until the target day in this week
383
+ days_diff = (target_wday - today_wday) % 7
384
+
385
+ # If it's the same day, return today's date
386
+ if days_diff == 0
387
+ return today
388
+ end
389
+
390
+ # Return the date of the next occurrence in this week
391
+ today + days_diff
392
+ end
393
+
394
+ # Calculate date for "next [day]" - the day in next week
395
+ def calculate_next_day(target_wday)
396
+ today = Date.today
397
+ today_wday = today.wday
398
+
399
+ # Calculate days until the next occurrence
400
+ days_until_target = (target_wday - today_wday) % 7
401
+
402
+ # If today is the target day or the target day is earlier in the week,
403
+ # we want the day next week, so add 7 days
404
+ if days_until_target == 0 || target_wday < today_wday
405
+ days_until_target += 7
406
+ end
407
+
408
+ today + days_until_target
409
+ end
410
+
411
+ # Calculate date for "last [day]" - the day in previous week
412
+ def calculate_last_day(target_wday)
413
+ today = Date.today
414
+ today_wday = today.wday
415
+
416
+ # Calculate days since the last occurrence
417
+ days_since_target = (today_wday - target_wday) % 7
418
+
419
+ # If today is the target day or the target day is later in the week,
420
+ # we want the day last week, so add 7 days
421
+ if days_since_target == 0 || target_wday > today_wday
422
+ days_since_target += 7
423
+ end
424
+
425
+ today - days_since_target
426
+ end
427
+
113
428
  # Calculate the date based on the keyword value
114
429
  def calculate_date(date_value)
115
430
  if date_value.is_a?(Integer)
@@ -120,20 +435,107 @@ module Hizuke
120
435
  # If today is Monday, we want next Monday, not today
121
436
  days_until_monday = 7 if days_until_monday == 0
122
437
  Date.today + days_until_monday
438
+ elsif date_value == :last_week
439
+ # Find last Monday
440
+ days_since_monday = (Date.today.wday - 1) % 7
441
+ # If today is Monday, we want last Monday, not today
442
+ days_since_monday = 7 if days_since_monday == 0
443
+ Date.today - days_since_monday - 7
123
444
  elsif date_value == :next_month
124
445
  # Return the first day of the next month
125
446
  next_month = Date.today >> 1
126
447
  Date.new(next_month.year, next_month.month, 1)
448
+ elsif date_value == :last_month
449
+ # Return the first day of the previous month
450
+ prev_month = Date.today << 1
451
+ Date.new(prev_month.year, prev_month.month, 1)
127
452
  elsif date_value == :next_year
128
453
  # Return the first day of the next year
129
454
  next_year = Date.today.year + 1
130
455
  Date.new(next_year, 1, 1)
456
+ elsif date_value == :last_year
457
+ # Return the first day of the last year
458
+ last_year = Date.today.year - 1
459
+ Date.new(last_year, 1, 1)
460
+ elsif date_value == :next_quarter
461
+ # Return the first day of the next quarter
462
+ today = Date.today
463
+ current_month = today.month
464
+
465
+ # Determine the start month of the next quarter
466
+ next_quarter_month = case
467
+ when current_month <= 3
468
+ 4 # Q2 starts in April
469
+ when current_month <= 6
470
+ 7 # Q3 starts in July
471
+ when current_month <= 9
472
+ 10 # Q4 starts in October
473
+ else
474
+ 1 # Q1 of next year starts in January
475
+ end
476
+
477
+ # If the next quarter is in the next year, increment the year
478
+ next_quarter_year = today.year
479
+ next_quarter_year += 1 if current_month > 9
480
+
481
+ Date.new(next_quarter_year, next_quarter_month, 1)
482
+ elsif date_value == :last_quarter
483
+ # Return the first day of the last quarter
484
+ today = Date.today
485
+ current_month = today.month
486
+
487
+ # Determine the start month of the last quarter
488
+ last_quarter_month = case
489
+ when current_month <= 3
490
+ 10 # Q4 of last year starts in October
491
+ when current_month <= 6
492
+ 1 # Q1 starts in January
493
+ when current_month <= 9
494
+ 4 # Q2 starts in April
495
+ else
496
+ 7 # Q3 starts in July
497
+ end
498
+
499
+ # If the last quarter is in the previous year, decrement the year
500
+ last_quarter_year = today.year
501
+ last_quarter_year -= 1 if current_month <= 3
502
+
503
+ Date.new(last_quarter_year, last_quarter_month, 1)
131
504
  elsif date_value == :this_weekend
132
505
  # Calculate days until Saturday
133
506
  days_until_saturday = (6 - Date.today.wday) % 7
134
507
  # If today is Saturday or Sunday, we're already on the weekend
135
508
  days_until_saturday = 0 if days_until_saturday == 0 || days_until_saturday == 6
136
509
  Date.today + days_until_saturday
510
+ elsif date_value == :end_of_week
511
+ # Calculate days until Sunday (end of week)
512
+ days_until_sunday = (0 - Date.today.wday) % 7
513
+ # If today is Sunday, we're already at the end of the week
514
+ days_until_sunday = 0 if days_until_sunday == 0
515
+ Date.today + days_until_sunday
516
+ elsif date_value == :end_of_month
517
+ # Return the last day of the current month
518
+ # Get the first day of next month
519
+ next_month = Date.today >> 1
520
+ first_day_next_month = Date.new(next_month.year, next_month.month, 1)
521
+ # Subtract one day to get the last day of current month
522
+ first_day_next_month - 1
523
+ elsif date_value == :end_of_year
524
+ # Return the last day of the current year (December 31)
525
+ Date.new(Date.today.year, 12, 31)
526
+ elsif date_value == :mid_week
527
+ # Return Wednesday of the current week
528
+ # Calculate days until/since Wednesday (3)
529
+ today_wday = Date.today.wday
530
+ target_wday = 3 # Wednesday
531
+ days_diff = (target_wday - today_wday) % 7
532
+ # If the difference is more than 3, then Wednesday has passed this week
533
+ # So we need to go back to Wednesday
534
+ days_diff = days_diff - 7 if days_diff > 3
535
+ Date.today + days_diff
536
+ elsif date_value == :mid_month
537
+ # Return the 15th day of the current month
538
+ Date.new(Date.today.year, Date.today.month, 15)
137
539
  end
138
540
  end
139
541
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hizuke
4
- VERSION = "0.0.2"
4
+ VERSION = "0.0.4"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hizuke
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Juraj Maťaše
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-04-23 00:00:00.000000000 Z
11
+ date: 2025-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -67,6 +67,7 @@ files:
67
67
  - LICENSE.txt
68
68
  - README.md
69
69
  - Rakefile
70
+ - hizuke.gemspec
70
71
  - lib/hizuke.rb
71
72
  - lib/hizuke/parser.rb
72
73
  - lib/hizuke/version.rb