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 +4 -4
- data/CHANGELOG.md +51 -0
- data/Gemfile.lock +1 -1
- data/README.md +111 -2
- data/hizuke.gemspec +36 -0
- data/lib/hizuke/parser.rb +416 -14
- data/lib/hizuke/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a44032e09036e25847a067394cf50b60f47197e5fe4d29b068c0e822960db9d
|
4
|
+
data.tar.gz: 8f6a2d515fe0b1b64e970ef5ced8aad5aaa96febfd6c44448dc765d78521a709
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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/
|
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
|
-
#
|
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
|
57
|
-
start_idx =
|
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
|
-
|
74
|
-
|
75
|
-
|
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(
|
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 =
|
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 '#{
|
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
|
-
|
269
|
+
final_text = clean_words.join(" ").strip
|
107
270
|
|
108
|
-
Result.new(
|
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
|
data/lib/hizuke/version.rb
CHANGED
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.
|
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-
|
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
|