npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nl2time

v0.3.1

Published

Bidirectional natural language ⇄ date/time: parse expressions like 'last week' into concrete intervals, and describe instants as '9pm last night' — deterministic core, optional LLM assist.

Readme

nl2time

Bidirectional natural language ⇄ date/time. Parse expressions like "last week" into concrete, timezone- and locale-correct intervals — and describe instants back as "9pm last night". Deterministic core; optional LLM assist that can only ever emit the same symbolic IR the rule parser emits.

import { TimeContext, parse, resolve, describe, Temporal } from 'nl2time';

const ctx = TimeContext.make({
  now: '2026-07-20T17:00:00Z',        // omit for system time; pass for reproducibility
  timeZone: 'America/Los_Angeles',    // the user's zone, not the machine's
  locale: 'en-US',                    // region matters: week start, date order, …
});

// NL → time -----------------------------------------------------------------
const { matches } = parse('how many shoes did I sell last week', ctx);
resolve(matches[0].expr, ctx).candidates[0];
// { kind: 'interval', start: 2026-07-12T07:00:00Z, end: 2026-07-19T07:00:00Z, grain: 'week' }
// …with locale 'en-GB' the same words give Jul 13 – Jul 20 (Monday week start).

// time → NL -----------------------------------------------------------------
describe(Temporal.Instant.from('2026-07-20T04:00:00Z'), ctx, { style: 'casual' }).text;
// '9pm last night'   (4am UTC Jul 20 = 9pm Jul 19 in LA; today is Jul 20 there)
describe(Temporal.Instant.from('2026-07-20T04:00:00Z'), ctx).text;
// 'yesterday at 9:00 PM'

New here? Read the step-by-step walkthrough — two fully-traced examples showing exactly what happens at each stage in both directions (tokens → rules → IR → deterministic resolution, and instant → framing → IR → rendered speech).

Why another date library?

Existing tools do one direction, half-way:

  • Parsers (chrono-node, Duckling, dateparser) collapse text straight to a concrete datetime, losing granularity ("July" is a month, not a millisecond), ambiguity (which "Friday"?), and the week-start question entirely.
  • Formatters (Intl.RelativeTimeFormat, Luxon, timeago) take a pre-computed (value, unit)you decide the framing, the unit, and when "23 hours ago" should become "yesterday". Nobody owns that decision.
  • LLMs are demonstrably bad at raw date arithmetic (leap years, DST, large offsets) but good at language.

nl2time puts a small symbolic IR in the middle of both directions:

   text ──parse──►┐                       ┌──◄─select── instant/interval
                  │   TimeExpr (JSON IR)  │
                  └──►─resolve──► value   └──render──► "9pm last night"
  • TimeExpr — a JSON operator tree (now, literal, offset, snap, span, seek, between, intersect, recur) over timeline intervals. Spec, JSON Schema.
  • resolve(expr, ctx) — deterministic evaluation against a TimeContext (reference instant, IANA timezone, locale, week start, ambiguity policies). Ambiguity is data: candidates come back ordered, never silently guessed.
  • describe(value, ctx) — chooses framing (calendar / elapsed / absolute), builds the IR expression that describes the value, renders it via Intl. Round-trip invariant: resolve(describe(v).expr, ctx) contains v.
  • Everything language refers to is an interval with a grain. "Last week" is seven days; "9pm" is an hour; "March" is a month.

The LLM boundary (optional)

nl2time never makes network calls. Rules run first; you can supply a fallback for the long tail, and its output is schema-validated IR, never concrete dates — all arithmetic stays deterministic and testable:

import { parseWithFallback, buildPrompt, irJsonSchema } from 'nl2time/llm';

const result = await parseWithFallback(text, ctx, async (text, ctxSummary) => {
  // call your LLM with buildPrompt(text, ctx) + irJsonSchema() constrained output
  return llmClient.structured(buildPrompt(text, ctx), irJsonSchema());
});

See docs/agents.md for reference architectures (analytics agents, tool-calling, deferred resolution).

Domain adaptation

Domains with conflicting or novel vocabulary ("FY26", "EOD means 5pm", "swing shift") extend the parser without forking via declarative JSON packs — phrase→IR templates with integer/year captures that compete with (and can shadow) the built-in rules, plus a disable list and a code-level Rule escape hatch. Packs ship their own golden cases, runnable with nl2time/corpus. See docs/extending.md and the worked fiscal-calendar example.

Policy, not guesses

Cultural/ambiguous semantics are explicit TimeContext knobs with CLDR-derived defaults:

| Question | Knob | Default | |---|---|---| | Does a week start Sunday or Monday? | weekStart | CLDR, by locale region (en-US: sun, en-GB: mon) | | Is 5/2 May 2nd or Feb 5th? | dateOrder | CLDR-style by region | | "Friday" — which one? | bias: 'past' \| 'future' \| 'none' | none (nearest) | | "Next Tuesday" on a Sunday? | nextWeekday: 'nearest' \| 'week-after' | nearest | | Does "last 3 days" include today? | partialPeriod: 'include' \| 'exclude' | include | | "At 4" — am or pm? | — | both candidates, plausibility-ordered |

Install

npm

npm install nl2time

Languages

Parsing is multilingual, dispatched by the context's locale. Each language climbs its own imported conformance corpus (Microsoft Recognizers-Text specs, MIT) with a CI-gated baseline:

| Language | Corpus cases | Passing | |---|---|---| | English (en-US / en-GB) | 1,031 | 84% | | German | 157 | 97% | | Japanese | 393 | 96% | | Chinese (Simplified) | 175 | 95% | | French | 406 | 90% | | Spanish | 579 | 96% |

Latin-script languages share a parameterized rule factory (makeLatinRules + a lexicon); CJK languages use per-character tokenization with bespoke rule modules. describe() output is currently English; localized rendering is on the roadmap. Remaining failure mass is dominated by documented upstream divergences (e.g. issue #14).

Status

v0.3 — multilingual parsing (6 locales), en-* describe, core engine with a DST/edge-case battery, holidays (fixed-date, nth-weekday, computed Easter), business-day spans, domain packs. Recurrence (every Tuesday) is representable in the IR but resolves in v2.

Corpus (corpus/, runner exported as nl2time/corpus): bidirectional golden sets with per-case license provenance — hand-authored forward + reverse sets (100% passing, including the machine-inverted reverse set), and ~2,700 gradeable imported cases across six languages with per-language CI regression baselines (npm run eval, npm run baselines). See corpus/ATTRIBUTIONS.md and docs/porting.md.

Test-data provenance

Every imported conformance case carries machine-readable provenance (upstream project, license, file path, pinned commit, index), and upstream licenses are vendored verbatim beside the data. corpus/ATTRIBUTIONS.md is the authoritative record: what is vendored (Microsoft Recognizers-Text, MIT), what is original (the hand-authored and machine-inverted golden sets), what is used eval-only and never redistributed (TempEval-3 Platinum, WikiWars), what was deliberately excluded on license grounds (GPL/LDC/ODbL sources), and the academic lineage (TimeML/TIMEX3, SCATE, TempEval-3, and the LLM date-arithmetic benchmarks that motivated the deterministic-IR design). Known upstream data defects are documented there too, with the exclusion rules applied at import.

Python

PyPI

pip install nl2time

The Python engine port lives in python/ (whenever-based time model): IR validation, TimeContext, and the full deterministic resolver at 100% bit-exact parity with the JS reference across all 2,760 engine-parity fixtures (corpus/ir/), enforced in CI on every push. Parsers and describe() are JS-only so far — docs/porting.md has the strategy and the divergence gates.

Release history

See CHANGELOG.md and GitHub releases. Current: v0.3.0 (JS, multilingual parsing) · py-v0.1.0 (Python engine). Releases publish to npm/PyPI automatically via OIDC trusted publishing — see docs/RELEASING.md.

Development

npm install
npm test          # vitest: conformance fixtures + DST battery + round-trip invariant
npm run typecheck
npm run build

MIT.