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 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
@@ -0,0 +1,3 @@
1
+ brew "rust"
2
+ brew "gh"
3
+ brew "hk"
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
@@ -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).