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.6"

Class Method Summary collapse

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