duckling 0.4.7-aarch64-linux
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/Brewfile +3 -0
- data/CHANGELOG.md +218 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/NOTICES +52 -0
- data/README.md +253 -0
- data/Rakefile +243 -0
- data/docs/2026-07-01-roadmap.md +106 -0
- data/docs/tz-database-axis.md +454 -0
- data/lib/duckling/3.2/duckling.so +0 -0
- data/lib/duckling/3.3/duckling.so +0 -0
- data/lib/duckling/3.4/duckling.so +0 -0
- data/lib/duckling/4.0/duckling.so +0 -0
- data/lib/duckling/tzinfo_capabilities.rb +59 -0
- data/lib/duckling/version.rb +5 -0
- data/lib/duckling.rb +284 -0
- metadata +121 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d7f01e7c1806a99f56a2eb7a2624849e3a378b16a007927da8e98fecaa9075cb
|
|
4
|
+
data.tar.gz: 59882deda0bb0bd9abb16d75c6d39cf1b3293a5b595c71578722d5efcc85e42f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1cd48c2532aa2f7ad1a34d91bcdacf1a4526946f1547ced4ca79cce2fff812f0794a8a10d37478a6ffd24ca22d313e0134ef79cde6020c0c693962a4bb1bbe35
|
|
7
|
+
data.tar.gz: b1926802b968fbee4bff4f896202aee26c0efbe7409e3b0ce4ff72043ea5ad020f2fdfa3eff8db9b1db568100fa0872a37860a185438a60463e965c4ff149ff6
|
data/Brewfile
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.4.6] - 2026-08-13
|
|
10
|
+
|
|
11
|
+
## What's Changed
|
|
12
|
+
* Benchmark results (github-actions, 0.4.5) by @cpb in https://github.com/cpb/duckling/pull/148
|
|
13
|
+
* Push only the source gem to GitHub Packages; release 0.4.6 by @cpb in https://github.com/cpb/duckling/pull/149
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
**Full Changelog**: https://github.com/cpb/duckling/compare/v0.4.5...v0.4.6
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## [0.4.5] - 2026-08-13
|
|
20
|
+
|
|
21
|
+
## What's Changed
|
|
22
|
+
* Benchmark results (github-actions, 0.4.4) by @github-actions[bot] in https://github.com/cpb/duckling/pull/146
|
|
23
|
+
* Fix release-time credential wiring; release 0.4.5 by @cpb in https://github.com/cpb/duckling/pull/147
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
**Full Changelog**: https://github.com/cpb/duckling/compare/v0.4.4...v0.4.5
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## [0.4.4] - 2026-08-13
|
|
30
|
+
|
|
31
|
+
## What's Changed
|
|
32
|
+
* Benchmark results (github-actions, 0.4.3) by @github-actions[bot] in https://github.com/cpb/duckling/pull/138
|
|
33
|
+
* Update CHANGELOG for v0.4.3 by @github-actions[bot] in https://github.com/cpb/duckling/pull/139
|
|
34
|
+
* Benchmark results (github-actions, 0.4.3) by @cpb in https://github.com/cpb/duckling/pull/141
|
|
35
|
+
* Default bin/worktree harness to Pi; add --pi/--claude/--gemini flags by @cpb in https://github.com/cpb/duckling/pull/142
|
|
36
|
+
* Document Pi as the default bin/worktree harness by @cpb in https://github.com/cpb/duckling/pull/143
|
|
37
|
+
* Skip the CI suite for docs-only PRs by @cpb in https://github.com/cpb/duckling/pull/144
|
|
38
|
+
* Publish release gems to GitHub Packages as well as RubyGems; release 0.4.4 by @cpb in https://github.com/cpb/duckling/pull/145
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
**Full Changelog**: https://github.com/cpb/duckling/compare/v0.4.3...v0.4.4
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## [0.4.3] - 2026-08-11
|
|
45
|
+
|
|
46
|
+
## What's Changed
|
|
47
|
+
* Update CHANGELOG for v0.4.2 by @github-actions[bot] in https://github.com/cpb/duckling/pull/134
|
|
48
|
+
* Benchmark results (github-actions, 0.3.0-0.4.2) by @cpb in https://github.com/cpb/duckling/pull/135
|
|
49
|
+
* Bump actions/download-artifact from 7.0.0 to 8.0.1 by @dependabot[bot] in https://github.com/cpb/duckling/pull/116
|
|
50
|
+
* Open release-automation PRs with a PAT so CI runs on them by @cpb in https://github.com/cpb/duckling/pull/136
|
|
51
|
+
* Require MFA for RubyGems operations; release 0.4.3 by @cpb in https://github.com/cpb/duckling/pull/137
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
**Full Changelog**: https://github.com/cpb/duckling/compare/v0.4.2...v0.4.3
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## [0.4.2] - 2026-08-11
|
|
58
|
+
|
|
59
|
+
## What's Changed
|
|
60
|
+
* Publish releases via RubyGems Trusted Publishing instead of a long-lived API key by @cpb in https://github.com/cpb/duckling/pull/131
|
|
61
|
+
* Bump Duckling::VERSION to 0.4.2 by @cpb in https://github.com/cpb/duckling/pull/132
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
**Full Changelog**: https://github.com/cpb/duckling/compare/v0.4.1...v0.4.2
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## [0.4.1] - 2026-08-10
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- The packaged gem no longer includes agent- and development-tooling files.
|
|
72
|
+
0.3.0 and 0.4.0 shipped `.claude/settings.json`, `AGENTS.md`, and
|
|
73
|
+
`CLAUDE.md`: the gemspec built its file list from `git ls-files` with a
|
|
74
|
+
reject-list of paths to exclude, so every newly tracked dotfile or tool
|
|
75
|
+
directory was packaged by default. The gemspec now allow-lists what ships
|
|
76
|
+
(`lib/`, `ext/`, `docs/` other than `docs/benchmarks/`, and a named set of
|
|
77
|
+
root files), so anything new stays out of the gem unless it is added
|
|
78
|
+
deliberately. Regression coverage checks the gemspec's file list in the
|
|
79
|
+
main suite and the built artifacts in `test/gem/packaged_gem_test.rb`.
|
|
80
|
+
None of the previously shipped files contained secrets — they are
|
|
81
|
+
development configuration and documentation, all public in the repository
|
|
82
|
+
— so the already-published 0.3.0/0.4.0 gems are unaffected in behavior and
|
|
83
|
+
have been left in place.
|
|
84
|
+
|
|
85
|
+
## [0.4.0] - 2026-08-10
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- **On a stock Debian/Ubuntu host, roughly a hundred IANA zone identifiers
|
|
90
|
+
stop resolving.** `reference_zone: "US/Eastern"` — and every other
|
|
91
|
+
backward-compatibility name, such as `"US/Pacific"`, `"Europe/Kiev"`, or
|
|
92
|
+
`"Japan"` — now raises `ArgumentError` there. Those names live in the
|
|
93
|
+
`tzdata-legacy` system package, which is not installed by default. Two ways
|
|
94
|
+
to get them back, either of which restores the previous behavior exactly:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
gem "tzinfo-data" # in your Gemfile
|
|
98
|
+
```
|
|
99
|
+
```bash
|
|
100
|
+
apt install tzdata-legacy # on the host
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Canonical identifiers (`"America/New_York"`, `"Europe/Kyiv"`) are
|
|
104
|
+
unaffected. A host with no zoneinfo files at all — a scratch or distroless
|
|
105
|
+
container — needs the gem for `reference_zone:` to work at all.
|
|
106
|
+
|
|
107
|
+
The error message names the tz database that answered, how many identifiers
|
|
108
|
+
it has, and both remedies, so this is distinguishable from a typo. The
|
|
109
|
+
datasource and the count describe whichever database answered on your host,
|
|
110
|
+
so both differ from the example below:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
invalid reference_zone: "US/Eastern" (resolved against system zoneinfo at
|
|
114
|
+
/usr/share/zoneinfo, which provides 497 identifiers; this database has no
|
|
115
|
+
backward-compat names (US/Eastern and ~100 others), so if that is what this
|
|
116
|
+
is, it needs either the tzinfo-data gem or the tzdata-legacy system package)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The remedy is worded as a condition rather than a claim about the name you
|
|
120
|
+
passed: whether a given identifier is one of the ~100 in IANA's `backward`
|
|
121
|
+
file isn't knowable without shipping that list, and asserting it would tell
|
|
122
|
+
every typo on such a host that `tzdata-legacy` will supply it.
|
|
123
|
+
|
|
124
|
+
A second, quieter difference comes with the same change: some distributions
|
|
125
|
+
compile tzdata in *rearguard* format, which strips negative DST, and on such
|
|
126
|
+
a host `Europe/Dublin` is modelled as an ordinary positive-DST zone rather
|
|
127
|
+
than a negative-DST one. Which distributions is not guessable — Ubuntu 24.04
|
|
128
|
+
is rearguard, Debian trixie is vanguard — so if you depend on tzinfo's
|
|
129
|
+
`dst?` flag, read it from the host rather than assuming. Resolved offsets
|
|
130
|
+
are the same either way, so no `Duckling.parse` result changes because of
|
|
131
|
+
it.
|
|
132
|
+
|
|
133
|
+
- `tzinfo-data` is no longer a runtime dependency. `tzinfo` already prefers
|
|
134
|
+
that gem when it is installed and falls back to the host's zoneinfo files
|
|
135
|
+
otherwise, so depending on it forced bundled tz data on every consumer to
|
|
136
|
+
serve the ones who want it. This is the change that produces the identifier
|
|
137
|
+
behavior above. Consumers who add `gem "tzinfo-data"` themselves get exactly
|
|
138
|
+
the previous behavior with no code change, and can still pick up a
|
|
139
|
+
tz-database revision by bumping that one gem. Dropping it means the bundled
|
|
140
|
+
tz data is no longer loaded at boot, so the first zone lookup does less
|
|
141
|
+
work; steady-state parsing is unaffected either way, since `reference_zone:`
|
|
142
|
+
resolution goes through the same tzinfo call once a database is loaded.
|
|
143
|
+
|
|
144
|
+
### Added
|
|
145
|
+
|
|
146
|
+
- `Duckling::TZDataUnavailable`, raised when `reference_zone:` is given on a
|
|
147
|
+
host with no tz database at all — no zoneinfo files and no `tzinfo-data`
|
|
148
|
+
gem, as in a scratch or distroless container. Newly reachable because of the
|
|
149
|
+
dependency change above; previously a database always existed. It names both
|
|
150
|
+
fixes, where the underlying tzinfo error mentioned neither this gem nor
|
|
151
|
+
`reference_zone:`. Deliberately not an `ArgumentError`: it reports the
|
|
152
|
+
deployment's state, not a bad argument, so code rescuing `ArgumentError`
|
|
153
|
+
around caller-supplied zone names does not swallow it. Every other keyword
|
|
154
|
+
works without a tz database.
|
|
155
|
+
|
|
156
|
+
## [0.3.0] - 2026-08-04
|
|
157
|
+
|
|
158
|
+
### Changed
|
|
159
|
+
|
|
160
|
+
- **Breaking:** `reference_time:` now requires a Ruby `Time` object (or
|
|
161
|
+
`nil`), not a Unix-seconds Integer. This lets the caller's `utc_offset` be
|
|
162
|
+
preserved into offset-aware `Instant` results (e.g. `"in one hour"`),
|
|
163
|
+
which previously always came back as UTC+0 regardless of the intended
|
|
164
|
+
anchor. Accepted values: a `Time`, or anything responding to `to_time`
|
|
165
|
+
(`ActiveSupport::TimeWithZone`, stdlib `DateTime`, etc.), which is coerced
|
|
166
|
+
automatically. Migrate a raw Integer by wrapping it in `Time.at(seconds)`.
|
|
167
|
+
- **Breaking:** a time result's `:value` (and an interval's `:from`/`:to`) is
|
|
168
|
+
now a real Ruby `Time`, not a formatted String. This applies to both
|
|
169
|
+
`Naive` (wall-clock, e.g. `"tomorrow"`, `"5pm"`) and `Instant` (e.g. `"in
|
|
170
|
+
one hour"`) results — `reference_time:`'s offset is now applied to
|
|
171
|
+
`Naive` results too, not just `Instant` ones. Callers parsing the old
|
|
172
|
+
ISO-ish string (with or without an offset suffix) should read `.value`
|
|
173
|
+
directly as a `Time` instead.
|
|
174
|
+
- **Breaking:** `:time`'s `:value` now uses the same unified,
|
|
175
|
+
externally-tagged shape as every other dimension: a single result is
|
|
176
|
+
`{Time: {Single: {value: {Naive:|Instant: {value:, grain:}}, values: [...],
|
|
177
|
+
holidayBeta: "..."}}}`, and an interval result is `{Time: {Interval: {from:
|
|
178
|
+
{Naive:|Instant: {...}}, to: {...}, values: [...]}}}`. Previously `:time`
|
|
179
|
+
kept its own bespoke flattened shape (`{type:, value:, grain:, values:}` /
|
|
180
|
+
`{type:, from:, to:}`) even after every other dimension moved onto the
|
|
181
|
+
tagged convention. `:value` is still always a real Ruby `Time`, never a
|
|
182
|
+
String, and `grain` is still the lowercase-snake_case symbol convention
|
|
183
|
+
(`:second`, `:no_grain`, ...) — only the wrapping shape changed. Migrate by
|
|
184
|
+
reaching through the new tags, e.g.
|
|
185
|
+
`entity[:value][:Time][:Single][:value][:Naive][:value]` in place of the
|
|
186
|
+
old `entity[:value][:value]`. One behavior change bundled with the shape
|
|
187
|
+
migration: an unbounded interval (e.g. `"after 3pm"`) now carries an
|
|
188
|
+
explicit `to: nil` (or `from: nil`) key instead of omitting the key
|
|
189
|
+
entirely — check `interval[:to].nil?` rather than `interval.key?(:to)`
|
|
190
|
+
to detect an unbounded endpoint.
|
|
191
|
+
|
|
192
|
+
## [0.2.0] - 2026-07-01
|
|
193
|
+
|
|
194
|
+
## What's Changed
|
|
195
|
+
* Ship duckling gem: time extraction via Magnus + wafer-inc-duckling by @cpb in https://github.com/cpb/duckling/pull/2
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
**Full Changelog**: https://github.com/cpb/duckling/compare/v0.1.2...v0.2.0
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
## [0.1.2] - 2026-07-01
|
|
202
|
+
|
|
203
|
+
## What's Changed
|
|
204
|
+
* Retry release pipeline as 0.1.2 by @cpb in https://github.com/cpb/duckling/pull/7
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
**Full Changelog**: https://github.com/cpb/duckling/compare/v0.1.1...v0.1.2
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
## [0.1.1] - 2026-07-01
|
|
211
|
+
|
|
212
|
+
## What's Changed
|
|
213
|
+
* Automate gem release: tag-triggered publish to RubyGems by @cpb in https://github.com/cpb/duckling/pull/5
|
|
214
|
+
|
|
215
|
+
## New Contributors
|
|
216
|
+
* @cpb made their first contribution in https://github.com/cpb/duckling/pull/5
|
|
217
|
+
|
|
218
|
+
**Full Changelog**: https://github.com/cpb/duckling/compare/v0.1.0...v0.1.1
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"duckling" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["me@cpb.ca"](mailto:"me@cpb.ca").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Caleb Buxton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/NOTICES
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Third-Party Notices
|
|
2
|
+
===================
|
|
3
|
+
|
|
4
|
+
The duckling gem's native extension (the compiled .bundle/.so — shipped
|
|
5
|
+
precompiled in the platform gems, built at install time from the source
|
|
6
|
+
gem) statically links the third-party component listed below. The gem
|
|
7
|
+
itself remains MIT-licensed (see LICENSE.txt); this file exists because
|
|
8
|
+
the component's BSD-3-Clause license requires binary redistributions to
|
|
9
|
+
reproduce its copyright notice, conditions, and disclaimer in the
|
|
10
|
+
documentation and/or other materials provided with the distribution.
|
|
11
|
+
|
|
12
|
+
Component: duckling (Rust crate)
|
|
13
|
+
Upstream: https://github.com/wafer-inc/duckling
|
|
14
|
+
License: BSD-3-Clause
|
|
15
|
+
Version: as locked in this repository's Cargo.lock
|
|
16
|
+
|
|
17
|
+
The license text below is reproduced verbatim from the component's
|
|
18
|
+
LICENSE file.
|
|
19
|
+
|
|
20
|
+
---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
BSD License
|
|
23
|
+
|
|
24
|
+
For Duckling software
|
|
25
|
+
|
|
26
|
+
Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
|
|
27
|
+
Rust port copyright (c) 2024-present, contributors.
|
|
28
|
+
|
|
29
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
30
|
+
are permitted provided that the following conditions are met:
|
|
31
|
+
|
|
32
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
33
|
+
list of conditions and the following disclaimer.
|
|
34
|
+
|
|
35
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
36
|
+
this list of conditions and the following disclaimer in the documentation
|
|
37
|
+
and/or other materials provided with the distribution.
|
|
38
|
+
|
|
39
|
+
* Neither the name Facebook nor the names of its contributors may be used to
|
|
40
|
+
endorse or promote products derived from this software without specific
|
|
41
|
+
prior written permission.
|
|
42
|
+
|
|
43
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
44
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
45
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
46
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
47
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
48
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
49
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
50
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
51
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
52
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Duckling
|
|
2
|
+
|
|
3
|
+
Ruby FFI adapter to a Rust [Duckling](https://github.com/wafer-inc/duckling) NER engine — no HTTP service required.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle add duckling
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install duckling
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "duckling"
|
|
23
|
+
|
|
24
|
+
Duckling.parse("tomorrow", locale: "en")
|
|
25
|
+
# =>
|
|
26
|
+
# [{ body: "tomorrow", start: 0, end: 8, dim: :time,
|
|
27
|
+
# value: { Time: { Single: { value: { Naive: { value: 2026-07-02 00:00:00 +0000, grain: :day } },
|
|
28
|
+
# values: [...] } } } }]
|
|
29
|
+
# (the date resolves relative to now; pass reference_time: for a fixed anchor)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`Duckling.parse` takes required text plus keyword options, and returns an
|
|
33
|
+
`Array` of entity `Hash`es (empty if nothing matched):
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
Duckling.parse(text, locale: "en", dims: ["time"], reference_time: nil, with_latent: false, reference_zone: nil)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Keyword arguments
|
|
40
|
+
|
|
41
|
+
- `locale:` (String, default `"en"`) — a `lang[-region]` tag, e.g. `"en"` or
|
|
42
|
+
`"en-GB"`. An unrecognized language or region raises `ArgumentError`.
|
|
43
|
+
- `dims:` (Array of String, default `["time"]`) — which dimensions to
|
|
44
|
+
extract. See "Supported dimensions" below. An unrecognized dimension name
|
|
45
|
+
raises `ArgumentError`.
|
|
46
|
+
- `reference_time:` (`Time`, default `nil`) — anchors relative expressions
|
|
47
|
+
like "tomorrow" or "next week". Its `utc_offset` is preserved into every
|
|
48
|
+
time result's `:value` — both wall-clock expressions ("tomorrow", "next
|
|
49
|
+
week") and offset-aware ones ("in one hour"), not flattened to UTC.
|
|
50
|
+
Defaults to the current UTC time; pass an explicit `Time` for deterministic
|
|
51
|
+
output. Anything else responding to `to_time` —
|
|
52
|
+
`ActiveSupport::TimeWithZone` (`Time.current`/`Time.zone.now`), stdlib
|
|
53
|
+
`DateTime`, etc. — is coerced to a `Time` automatically. A value that's
|
|
54
|
+
neither a `Time` nor `to_time`-able (e.g. a raw Integer Unix timestamp)
|
|
55
|
+
raises `TypeError`; wrap it in `Time.at(seconds)` first.
|
|
56
|
+
- `with_latent:` (Boolean, default `false`) — include ambiguous/latent
|
|
57
|
+
matches (e.g. a bare "morning") in the results.
|
|
58
|
+
- `reference_zone:` (String, default `nil`) — an IANA zone name, e.g.
|
|
59
|
+
`"America/New_York"`. Resolves each wall-clock result's UTC offset against
|
|
60
|
+
that zone on the result's *own* date, so a result before a DST transition
|
|
61
|
+
and one after it get different offsets instead of sharing
|
|
62
|
+
`reference_time:`'s single fixed one. An unknown identifier raises
|
|
63
|
+
`ArgumentError`; so does a `reference_time:` whose `utc_offset` disagrees
|
|
64
|
+
with the zone at that instant. It does not anchor the parse — see "Time
|
|
65
|
+
zone data" below.
|
|
66
|
+
|
|
67
|
+
There is no `Duckling::Error` class — invalid `locale:`/`dims:` values raise
|
|
68
|
+
plain `ArgumentError`, as do an unknown `reference_zone:` and a
|
|
69
|
+
`reference_time:` whose offset disagrees with it. The one named error a caller
|
|
70
|
+
is likely to meet is `Duckling::TZDataUnavailable`, for a host with no tz
|
|
71
|
+
database at all — see "Time zone data" below.
|
|
72
|
+
|
|
73
|
+
### Time zone data
|
|
74
|
+
|
|
75
|
+
`reference_zone:` resolves against [tzinfo][], which uses the `tzinfo-data`
|
|
76
|
+
gem when it is installed and the host's own zoneinfo files otherwise. This gem
|
|
77
|
+
does not depend on `tzinfo-data`, so by default you get the host's database.
|
|
78
|
+
|
|
79
|
+
That is usually what you want, and it is the faster of the two to start up.
|
|
80
|
+
Two cases where it is not:
|
|
81
|
+
|
|
82
|
+
- **Backward-compatibility names.** Debian and Ubuntu ship names like
|
|
83
|
+
`"US/Eastern"` in a separate `tzdata-legacy` package that is not installed
|
|
84
|
+
by default, so roughly a hundred valid IANA identifiers raise
|
|
85
|
+
`ArgumentError` on a stock host. Either `gem "tzinfo-data"` or
|
|
86
|
+
`apt install tzdata-legacy` restores them. The error message names whichever
|
|
87
|
+
database answered and how many identifiers it has, so you can tell this
|
|
88
|
+
apart from a typo.
|
|
89
|
+
- **No zoneinfo files at all**, as in a scratch or distroless container.
|
|
90
|
+
`reference_zone:` raises `Duckling::TZDataUnavailable` there, naming both
|
|
91
|
+
fixes. Add `gem "tzinfo-data"` to bundle the data with your app, where you
|
|
92
|
+
can also patch its vintage by bumping one gem, or install the system
|
|
93
|
+
`tzdata` package. Every other keyword works without a tz database.
|
|
94
|
+
|
|
95
|
+
`reference_zone:` reinterprets result offsets; it does not anchor the parse.
|
|
96
|
+
Given without `reference_time:`, a relative expression like `"tomorrow"` still
|
|
97
|
+
anchors on the machine-local clock rather than on "now" in that zone — pass a
|
|
98
|
+
`reference_time:` in the zone to anchor as well.
|
|
99
|
+
|
|
100
|
+
[tzinfo]: https://github.com/tzinfo/tzinfo
|
|
101
|
+
|
|
102
|
+
### Return value
|
|
103
|
+
|
|
104
|
+
Each entity in the returned array is a `Hash` with:
|
|
105
|
+
|
|
106
|
+
- `:body` (String) — the matched substring.
|
|
107
|
+
- `:start` / `:end` (Integer) — character offsets into the input text.
|
|
108
|
+
- `:dim` (Symbol) — the dimension, e.g. `:time`.
|
|
109
|
+
- `:latent` (Boolean) — present only when the match is latent.
|
|
110
|
+
- `:value` — every entity carries one; its shape depends on `:dim`.
|
|
111
|
+
|
|
112
|
+
Every dimension's `:value` is serde's externally-tagged representation of
|
|
113
|
+
the underlying Rust value, with all Hash keys symbolized — a one-pair
|
|
114
|
+
`Hash` keyed by a PascalCase tag matching the dimension (`{Numeral: 42.0}`,
|
|
115
|
+
`{Email: "user@example.com"}`, `{Url: {value: "...", domain: "..."}}`,
|
|
116
|
+
`{Temperature: {Value: {value: 37.0, unit: "celsius"}}}`). See
|
|
117
|
+
`test/duckling_parse_dimensions_test.rb` for the exact shape of every
|
|
118
|
+
dimension.
|
|
119
|
+
|
|
120
|
+
`:time` is the one dimension with additional nested tagging, since its
|
|
121
|
+
underlying leaf datetimes aren't serde-serializable as Ruby `Time`
|
|
122
|
+
objects and need patching after the fact (see `ext/duckling/src/lib.rs`'s
|
|
123
|
+
`patch_time_value`/`patch_time_point`):
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# a single point in time, e.g. "tomorrow"
|
|
127
|
+
{ Time: { Single: { value: { Naive: { value: 2026-07-02 00:00:00 +0000, grain: :day } },
|
|
128
|
+
values: [...] } } }
|
|
129
|
+
|
|
130
|
+
# an interval, e.g. "from 3pm to 5pm"
|
|
131
|
+
{ Time: { Interval: {
|
|
132
|
+
from: { Naive: { value: 2013-02-12 15:00:00 -0200, grain: :hour } },
|
|
133
|
+
to: { Naive: { value: 2013-02-12 18:00:00 -0200, grain: :hour } },
|
|
134
|
+
values: [...] } } }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Every `TimePoint` (the primary `value:`, each `values:` recurrence entry,
|
|
138
|
+
and an interval's `from:`/`to:`) is tagged `Naive` (wall-clock, no
|
|
139
|
+
timezone assumption — e.g. "tomorrow", "5pm") or `Instant` (offset-aware —
|
|
140
|
+
e.g. "in one hour", "now"). `grain` is one of `second`, `minute`, `hour`,
|
|
141
|
+
`day`, `week`, `month`, `quarter`, `year`. The nested `value:` is always a
|
|
142
|
+
real Ruby `Time`, not a formatted string — its `utc_offset` matches
|
|
143
|
+
`reference_time:`'s (or UTC, if `reference_time:` was omitted). A holiday
|
|
144
|
+
match (e.g. "christmas") additionally carries `holidayBeta:` (a String)
|
|
145
|
+
alongside `Single`'s `value:`/`values:`.
|
|
146
|
+
|
|
147
|
+
**Gotcha:** an interval's `:to` is the *exclusive* boundary, not the
|
|
148
|
+
literal named time — `"from 3pm to 5pm"` resolves `:to` to `18:00`, not
|
|
149
|
+
`17:00`. This matches upstream [duckling](https://github.com/wafer-inc/duckling)
|
|
150
|
+
behavior.
|
|
151
|
+
|
|
152
|
+
**Gotcha:** an unbounded interval (e.g. `"after 3pm"`, `"until 5pm"`)
|
|
153
|
+
still carries both `:from` and `:to` keys — the missing bound is an
|
|
154
|
+
explicit `nil` value, not an absent key. Check `interval[:to].nil?`
|
|
155
|
+
(not `interval.key?(:to)`) to detect an unbounded endpoint.
|
|
156
|
+
|
|
157
|
+
### Supported dimensions
|
|
158
|
+
|
|
159
|
+
`dims:` accepts `time`, `number`, `ordinal`, `temperature`, `distance`,
|
|
160
|
+
`volume`, `quantity`, `amount-of-money`, `email`, `phone-number`, `url`,
|
|
161
|
+
`credit-card-number`, `time-grain`, and `duration` — every entity comes back
|
|
162
|
+
with a populated `:value` (see "Return value" above for the shape). An
|
|
163
|
+
unrecognized dimension name raises `ArgumentError`.
|
|
164
|
+
|
|
165
|
+
### Known limitation: bare comma-separated lists
|
|
166
|
+
|
|
167
|
+
A run of date/time expressions joined by bare commas, with nothing else
|
|
168
|
+
between them, collapses into a single entity — every date after the first
|
|
169
|
+
in that run is silently dropped:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
Duckling.parse("birthdays are march 3, march 9, april 12 and may 5", locale: "en")
|
|
173
|
+
.select { |r| r[:dim] == :time }
|
|
174
|
+
.map { |r| r[:value][:value] }
|
|
175
|
+
# => [2013-03-03 00:00:00 +0000, 2013-05-05 00:00:00 +0000]
|
|
176
|
+
# (march 9 and april 12 are silently dropped)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This is an upstream grammar/ranking behavior in the wrapped
|
|
180
|
+
[duckling](https://github.com/wafer-inc/duckling) engine, not something this
|
|
181
|
+
gem can work around. Joining
|
|
182
|
+
dates with "and", periods, or a name/label immediately before each date
|
|
183
|
+
avoids the collapse:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
Duckling.parse("march 3 and march 9 and april 12 and may 5", locale: "en")
|
|
187
|
+
.select { |r| r[:dim] == :time }
|
|
188
|
+
.map { |r| r[:value][:value] }
|
|
189
|
+
# => [2013-03-03 00:00:00 +0000, 2013-03-09 00:00:00 +0000, 2013-04-12 00:00:00 +0000, 2013-05-05 00:00:00 +0000]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
See `test/duckling_comma_list_test.rb` for the full characterization,
|
|
193
|
+
including cases where the surviving value isn't even reliably the leftmost
|
|
194
|
+
date in the collapsed run.
|
|
195
|
+
|
|
196
|
+
## Performance
|
|
197
|
+
|
|
198
|
+
Benchmarked with [benchmark-ips](https://github.com/evanphx/benchmark-ips)
|
|
199
|
+
against `Duckling.parse`, including Magnus/Ruby conversion overhead (not
|
|
200
|
+
just the underlying Rust engine), plus GC pressure and threaded-worker-pool
|
|
201
|
+
throughput. See [`docs/benchmarks/`](docs/benchmarks/) for the latest
|
|
202
|
+
numbers, broken out by environment (GitHub Actions CI, Claude Code Web,
|
|
203
|
+
local dev) — results vary enough by machine that comparing across
|
|
204
|
+
environments is more meaningful than a single blended trend.
|
|
205
|
+
|
|
206
|
+
## Development
|
|
207
|
+
|
|
208
|
+
After checking out the repo, run `bin/setup` to install dependencies. Building
|
|
209
|
+
the native Rust extension requires a Rust toolchain; on macOS with Homebrew
|
|
210
|
+
installed, `bin/setup` installs it automatically via `brew bundle` and the
|
|
211
|
+
project's `Brewfile` (no-op if Homebrew isn't present). Then run
|
|
212
|
+
`rake compile` to build the extension before running `rake test`, or just run
|
|
213
|
+
`rake` (or `bundle exec rake`) with no arguments to lint, compile, and test in
|
|
214
|
+
order. You can also run `bin/console` for an interactive prompt that will
|
|
215
|
+
allow you to experiment.
|
|
216
|
+
|
|
217
|
+
`bin/setup` also seeds a `.env.local` file (from `.env.local.example`) with
|
|
218
|
+
`RB_SYS_CARGO_PROFILE=dev`, so local `rake compile` runs build the extension
|
|
219
|
+
in Cargo's dev profile by default — slower at runtime, but much faster to
|
|
220
|
+
compile while iterating. `.env.local` is gitignored and untouched by CI, so
|
|
221
|
+
CI and `rake release` still build the optimized release profile. Delete or
|
|
222
|
+
edit `.env.local` to opt back into a release-profile local build, or run
|
|
223
|
+
`bundle exec rake dev compile test` for a one-off dev-profile build without
|
|
224
|
+
`.env.local` in place.
|
|
225
|
+
|
|
226
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version: bump `Duckling::VERSION` in `version.rb`, merge that change to `main`, then run `bundle exec rake release` (or push a matching `vX.Y.Z` tag directly) to create and push the git tag. Pushing the tag triggers a GitHub Actions pipeline that re-runs CI as a gate, cross-compiles `x86_64-linux`/`aarch64-linux`/`x86_64-darwin`/`arm64-darwin` binary gems, verifies the tag matches `Duckling::VERSION`, builds and publishes the gems (source + one binary gem per platform) to [rubygems.org](https://rubygems.org) plus the source gem to the [GitHub Packages RubyGems registry](https://rubygems.pkg.github.com/cpb) (which keys uniqueness on name+version and so cannot hold the platform gems), cuts a GitHub release, and opens a PR appending an entry to `CHANGELOG.md`.
|
|
227
|
+
|
|
228
|
+
`bin/benchmark` (or `bundle exec rake benchmark`) runs the `benchmark-ips`
|
|
229
|
+
suite locally and prints results to the console — no files written.
|
|
230
|
+
`bin/benchmark record` (or `rake benchmark:record`) additionally writes
|
|
231
|
+
`docs/benchmarks/<environment>/<version>.json` and regenerates
|
|
232
|
+
`docs/benchmarks/README.md`. `bin/benchmark record-pr` (or `rake
|
|
233
|
+
benchmark:record_pr`) does the same against a fresh branch off `origin/main`
|
|
234
|
+
and opens (and auto-merges) a PR via `gh` — this is what the release
|
|
235
|
+
pipeline runs automatically, and what you'd also run from a Claude Code Web
|
|
236
|
+
session or a local dev machine to contribute that environment's numbers
|
|
237
|
+
ahead of a release. `gh` needs to be installed (`bin/setup` does this via
|
|
238
|
+
the `Brewfile` on macOS) and authenticated (`gh auth login`) for the
|
|
239
|
+
`record-pr` variant.
|
|
240
|
+
|
|
241
|
+
## Contributing
|
|
242
|
+
|
|
243
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/cpb/duckling. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/cpb/duckling/blob/master/CODE_OF_CONDUCT.md).
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
248
|
+
|
|
249
|
+
The native extension statically links the [duckling](https://github.com/wafer-inc/duckling) Rust crate (a Rust port of Facebook's Duckling), which is licensed under the BSD 3-Clause License. Its copyright notice and disclaimer are reproduced in [NOTICES](NOTICES), as that license requires of binary redistributions.
|
|
250
|
+
|
|
251
|
+
## Code of Conduct
|
|
252
|
+
|
|
253
|
+
Everyone interacting in the Duckling project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/cpb/duckling/blob/master/CODE_OF_CONDUCT.md).
|