Module: Duckling
- Defined in:
- lib/duckling.rb,
lib/duckling/version.rb,
lib/duckling/tzinfo_capabilities.rb
Defined Under Namespace
Modules: TZInfoCapabilities Classes: ShapeError, TZDataUnavailable
Constant Summary collapse
- VERSION =
"0.4.7"
Class Method Summary collapse
-
.apply_reference_zone(entities, reference_zone) ⇒ Object
Reinterprets every TimePoint::Naive (wall-clock) leaf of each :time entity against
reference_zone, using the real IANA offset for that leaf's own date. -
.parse(text, locale: "en", dims: ["time"], reference_time: nil, with_latent: false, reference_zone: nil) ⇒ Object
Native.parse already releases the GVL around the native call, but a bare GVL release alone does not hand control back to an Async::Reactor — Ruby 3.4's Fiber::Scheduler#blocking_operation_wait auto-offload path requires a flag rb_thread_call_without_gvl never sets.
Class Method Details
.apply_reference_zone(entities, reference_zone) ⇒ Object
Reinterprets every TimePoint::Naive (wall-clock) leaf of each :time entity
against reference_zone, using the real IANA offset for that leaf's own
date. reference_time: carries a single fixed offset, which cannot be right
for every leaf.
TimePoint::Instant leaves are left strictly alone: the wrapped crate already collapsed their relative arithmetic against one FixedOffset before this gem ever saw the result, so there is no wall-clock left to reinterpret. That arithmetic's DST imprecision is known and out of scope (issue #83).
Walks the externally-tagged shape ext/duckling/src/lib.rs's patch_time_value produces, and raises on any tag it doesn't recognize: a shape drift on the Rust side must fail loudly here. The outcome to prevent is quiet results resolved against the wrong offset.
133 134 135 136 137 |
# File 'lib/duckling.rb', line 133 def self.apply_reference_zone(entities, reference_zone) return entities unless reference_zone reinterpret_entities!(entities, timezone_for(reference_zone)) end |
.parse(text, locale: "en", dims: ["time"], reference_time: nil, with_latent: false, reference_zone: nil) ⇒ Object
Native.parse already releases the GVL around the native call, but a bare GVL release alone does not hand control back to an Async::Reactor — Ruby 3.4's Fiber::Scheduler#blocking_operation_wait auto-offload path requires a flag rb_thread_call_without_gvl never sets. Spawning a real background Thread lets the calling Fiber yield to the reactor via Thread#value's block/unblock scheduler hooks instead, which have been present since Ruby 3.0. See https://github.com/cpb/duckling/wiki/research-fiber-scheduler-mechanism-spike for the empirical result driving this.
Only worth paying for when a Fiber scheduler is actually installed on the calling thread: a plain thread pool (Puma/Sidekiq-style, no reactor to yield to) already gets its concurrency from Native.parse's own GVL release, so the extra Thread.new there is a pure spawn+join tax. Calling Native.parse directly (no thread) is also the benchmark suite's baseline for measuring the dispatch overhead itself.
report_on_exception is disabled from the very first line inside the spawned thread (not set on the Thread object afterward, which would race a fast-failing call) so a rescued error doesn't also print a thread-termination backtrace to stderr — Thread#value still re-raises it to the caller as ordinary control flow.
reference_time: is coerced here because the native extension cannot: Native.parse's Magnus binding only accepts a strict kind_of?(Time) (issue #45), which rejects ActiveSupport::TimeWithZone and stdlib DateTime even though both carry the same to_i/utc_offset a real Time does — #to_time normalizes any of those (and anything else that offers the same conversion) to a real Time before it crosses into Rust. reference_zone: never crosses into Native.parse — the wrapped Rust crate has no IANA-zone concept at all, only the single FixedOffset it derives from reference_time:. Per-date-correct offsets therefore have to come from a real tz database on the Ruby side, so reference_zone: is applied as a two-part step around the native call: validate the zone (and reference_time:'s agreement with it) before, then reinterpret Naive results after.
A fixed offset and a zone that disagree at the reference instant have no principled resolution — silently preferring either would resolve results against an offset the caller never asked for — so that combination raises.
reference_zone: only reinterprets result offsets after the fact; it does NOT anchor the parse. Given without reference_time:, relative expressions ("tomorrow") still anchor on the machine-local clock. They do not anchor on "now" in that zone, so on a US host reference_zone: "Asia/Tokyo" can land on the wrong calendar day. Pass a reference_time: in the zone to anchor as well.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/duckling.rb', line 96 def self.parse(text, locale: "en", dims: ["time"], reference_time: nil, with_latent: false, reference_zone: nil) reference_time = reference_time.to_time if reference_time && !reference_time.is_a?(Time) && reference_time.respond_to?(:to_time) if reference_zone zone = timezone_for(reference_zone) verify_reference_time_offset!(reference_time, zone, reference_zone) if reference_time end kwargs = {locale: locale, dims: dims, with_latent: with_latent} kwargs[:reference_time] = reference_time if reference_time entities = if Fiber.scheduler Thread.new do Thread.current.report_on_exception = false Native.parse(text, **kwargs) end.value else Native.parse(text, **kwargs) end zone ? reinterpret_entities!(entities, zone) : entities end |