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

isotropic-temporal-format

v0.1.0

Published

Format and parse Temporal values with template patterns

Readme

isotropic-temporal-format

npm version License

Formats Temporal values to strings and parses string to Temporal values with custom template patterns.

Why Use This?

  • Familiar Patterns: Uses the same to_char/to_timestamp template patterns as PostgreSQL (YYYY, HH24, MI, FF6, Month, Day, and so on)
  • Every Temporal Type: Formats Instant, ZonedDateTime, PlainDateTime, PlainDate, PlainTime, PlainYearMonth, PlainMonthDay, and Duration
  • Two-Way: toString renders a Temporal value to a string and fromString parses a string back into a Temporal value
  • Built on Temporal: Uses the native Temporal API for accurate, dependency-free date and time math
  • Type-Checked: A pattern that cannot apply to a value (a time pattern on a PlainDate, an offset on a PlainTime) raises a FormatError instead of silently emitting the pattern name
  • Extensible: Patterns are ordinary objects, so you can add your own by subclassing with an additional pattern set (see Custom Patterns)

Requirements

This package depends on the global Temporal object and ships as native ES modules. It targets newer Node.js versions, where Temporal is available without a flag.

Installation

npm install isotropic-temporal-format

Usage

import _TemporalFormat from 'isotropic-temporal-format';

// Format a PlainDateTime
_TemporalFormat.toString({
    format: 'YYYY-MM-DD HH24:MI:SS',
    value: Temporal.PlainDateTime.from('2024-01-15T09:30:45')
});
// '2024-01-15 09:30:45'

// Format a Duration (sub-day units fold into hours)
_TemporalFormat.toString({
    format: 'HH24:MI:SS',
    value: Temporal.Duration.from({
        minutes: 150
    })
});
// '02:30:00'

// Parse a string back into a Temporal value
_TemporalFormat.fromString({
    format: 'YYYY-MM-DD"T"HH24:MI:SS',
    string: '2024-01-15T09:30:45',
    typeName: 'PlainDateTime'
});
// Temporal.PlainDateTime <2024-01-15T09:30:45>

API

fromString(options)

Parses a string into a Temporal value.

| Option | Type | Default | Description | | --- | --- | --- | --- | | format | string | — | A template string describing the input | | strict | boolean | false | Opt-in to strict parsing | | string | string | — | The input to parse | | typeName | string | 'Instant' | The name of the Temporal type to produce |

Returns an instance of the requested type.

typeName is the string name of the target type: 'Duration', 'Instant', 'PlainDate', 'PlainDateTime', 'PlainMonthDay', 'PlainTime', 'PlainYearMonth', or 'ZonedDateTime'.

toString(options)

Renders a Temporal value to a string.

| Option | Type | Description | | --- | --- | --- | | format | string | A template string of patterns, modifiers, and literal text | | value | Temporal.* | Any supported Temporal value |

Returns the formatted string.

value may be a Temporal.Duration, Temporal.Instant, Temporal.PlainDate, Temporal.PlainDateTime, Temporal.PlainMonthDay, Temporal.PlainTime, Temporal.PlainYearMonth, or Temporal.ZonedDateTime.

Format Patterns

These mostly follow PostgreSQL's date/time template patterns. Case matters: the case of a name pattern (such as Month versus MONTH) controls the case of the output.

Hour, minute, second

| Pattern | Description | | --- | --- | | HH24 | Hour of day on a 24-hour clock (00-23); for a Duration, the total folded hours, which may exceed 23 | | HH, HH12 | Hour of day on a 12-hour clock (01-12) | | MI | Minute (00-59) | | SS | Second (00-59) | | SSSS, SSSSS | Seconds past midnight; for a Duration, total folded seconds | | FF1 | Tenth of Second (0-9) | | FF2 | Hundredth of Second (00-99) | | FF3, MS | Millisecond (000-999) | | FF4 | Tenth of Millisecond (0000-9999) | | FF5 | Hundredth of Millisecond (00000-99999) | | FF6, US | Microsecond (000000-999999) | | FF7 | Tenth of Microsecond (0000000-9999999) | | FF8 | Hundredth of Microsecond (00000000-99999999) | | FF9, NS | Nanosecond (000000000-999999999) |

FF7, FF8, FF9, and NS are extensions beyond PostgreSQL, which stops at microseconds; Temporal values carry nanosecond precision, so the full resolution is available here.

Fractional seconds are rounded, not truncated. FF1 through FF6, MS, and US first round the subsecond value to microseconds and then take the leading digits, matching PostgreSQL's to_char, so .123456789 renders as 123457 with FF6 and 1234 with FF4. FF7 through FF9 round at their own width. Rounding never carries into the seconds: .999999999 renders as 999999 with FF6, not 000000.

Meridiem

| Pattern | Description | | --- | --- | | AM, PM | Uppercase meridiem indicator (AM/PM) | | am, pm | Lowercase meridiem indicator (am/pm) | | A.M., P.M. | Uppercase meridiem indicator with periods | | a.m., p.m. | Lowercase meridiem indicator with periods |

AM and PM are interchangeable on input and both render the value's actual meridiem.

Year

| Pattern | Description | | --- | --- | | YYYY | Year, four or more digits | | YYY, YY, Y | Last 3, 2, or 1 digits of the year | | Y,YYY | Year with a comma thousands separator | | IYYY | ISO 8601 week-numbering year | | IYY, IY, I | Last 3, 2, or 1 digits of the ISO year | | CC | Century |

Era

| Pattern | Description | | --- | --- | | AD, BC | Uppercase era indicator | | ad, bc | Lowercase era indicator | | A.D., B.C. | Uppercase era indicator with periods | | a.d., b.c. | Lowercase era indicator with periods |

The era patterns are interchangeable on input and all render the value's actual era. Years are interpreted astronomically: year 0 renders as 1 BC, year -1 as 2 BC, and so on.

Month

| Pattern | Description | | --- | --- | | MM | Month number (01-12) | | MONTH, Month, month | Full month name, blank-padded to nine characters (Use the FM modifier to remove the padding.) | | MON, Mon, mon | Three-letter month abbreviation | | RM, rm | Month in Roman numerals (I-XII / i-xii), blank-padded to four characters (Use the FM modifier to remove the padding.) | | Q | Quarter of the year (1-4) |

Day

| Pattern | Description | | --- | --- | | DD | Day of month (01-31) | | DDD | Day of year (001-366) | | IDDD | ISO day of year (001-371) | | D | Day of week, Sunday = 1 through Saturday = 7 | | ID | ISO day of week, Monday = 1 through Sunday = 7 | | DAY, Day, day | Full weekday name, blank-padded to nine characters (Use the FM modifier to remove the padding.) | | DY, Dy, dy | Three-letter weekday abbreviation | | J | Julian day (number of days since November 24, 4714 BC, in the proleptic Gregorian calendar) |

Week

| Pattern | Description | | --- | --- | | W | Week of month (1-5) | | WW | Week of year (01-53) | | IW | ISO week of year (01-53) |

Time zone

These apply only to Temporal.Instant and Temporal.ZonedDateTime.

| Pattern | Description | | --- | --- | | TIMEZONE, TimeZone, timezone | Time zone name (for example America/New_York / america/new_york) | | TZ, tz | Time zone abbreviation, uppercase or lowercase (for example EST / est) | | TZH | Time zone offset hours (for example +05) | | TZM | Time zone offset minutes (for example 30) | | OF | Time zone offset (for example +05:30, or +00 when there are no offset minutes) |

TimeZone renders the value's time zone identifier, with the pattern's case controlling the output case. TZ renders the localized abbreviation (EST, PDT, GMT+5:30, and so on).

Unlike PostgreSQL, which stores only offsets, these patterns also parse time zone names (see Time zone parsing).

Modifiers

| Pattern | Position | Description | | --- | --- | --- | | FM | prefix | Fill mode: suppress leading zeros and blank padding for the pattern that immediately follows | | FX | global prefix | Fixed format: switches parsing to consume input strictly by position (see Parsing) | | TH, th | suffix | Append an uppercase or lowercase ordinal suffix (for example DDth produces 15th) | | TM | prefix | Translation mode: accepted but not implemented, so it is ignored | | SP | suffix | Spell mode: accepted but not implemented, so it is ignored |

_TemporalFormat.toString({
    format: 'FMDay, FMMonth FMDDth, YYYY',
    value: Temporal.PlainDate.from('2024-01-15')
});
// 'Monday, January 15th, 2024'

Literals and Escapes

| Pattern | Description | | --- | --- | | "..." | Literal text, copied verbatim | | \" | Literal quote character | | \\ | Literal backslash, only recognized within quoted literal text (Outside of quoted literal text, backslashes act as regular characters and do not require escaping.)|

_TemporalFormat.toString({
    format: '"Year:" YYYY',
    value: Temporal.PlainDate.from('2024-01-15')
});
// 'Year: 2024'

Type Validation

Each pattern declares the value components it needs, and each Temporal type declares the components it can provide. When a format uses a pattern the value cannot satisfy, toString and fromString both throw a FormatError rather than guessing. For example, time patterns have no meaning for a PlainDate:

_TemporalFormat.toString({
    format: 'YYYY-MM-DD HH24:MI',
    value: Temporal.PlainDate.from('2024-01-15')
});
// throws FormatError: pattern 'HH24' is not valid for the 'PlainDate' type

Calendars

Only the ISO 8601 calendar is supported. A value in any other calendar system raises a CalendarError; convert it with withCalendar('iso8601') first if that is your intent.

_TemporalFormat.toString({
    format: 'YYYY-MM-DD',
    value: Temporal.PlainDate.from('2024-01-15').withCalendar('hebrew')
});
// throws CalendarError

Instants

A Temporal.Instant has no associated time zone, so it is rendered in UTC. To render an instant in another zone, convert it to a Temporal.ZonedDateTime first:

_TemporalFormat.toString({
    format: 'YYYY-MM-DD HH24:MI:SS TZ',
    value: Temporal.Instant.from('2024-01-15T14:30:00Z').toZonedDateTimeISO('America/New_York')
});
// '2024-01-15 09:30:00 EST'

Durations

Temporal.Duration values are rendered with the same time and date patterns:

  • Sub-day units (hours, minutes, seconds, and fractional seconds) are summed and redistributed. HH24:MI:SS of a 25-hour-61-minute duration is 26:01:00, and HH24 may exceed 23. HH/HH12 still wrap to the 12-hour clock.
  • Weeks fold into days. Days are not folded into hours, so DD shows weeks * 7 + days while HH24 shows only the sub-day hours.
  • Months normalize into years: YYYY is the whole number of years and MM is the remaining months.
  • The magnitude is rendered; the sign is dropped. Read duration.sign separately if you need it.
  • Patterns that have no meaning for a duration (weekday and month names, J, Q, TZ, week patterns, era indicators, and so on) raise a FormatError, since a duration carries no calendar date or wall-clock context to resolve them. The numeric MM still works because it is just the month count.
_TemporalFormat.toString({
    format: 'DD "days" HH24:MI:SS',
    value: Temporal.Duration.from({
        days: 2,
        hours: 25,
        minutes: 61
    })
});
// '02 days 26:01:00'

Parsing

fromString reads a string according to a format and assembles a value of the requested typeName.

_TemporalFormat.fromString({
    format: 'Month DD, YYYY',
    string: 'March 15, 2026',
    typeName: 'PlainDate'
});
// Temporal.PlainDate <2026-03-15>

_TemporalFormat.fromString({
    format: 'IYYY-"W"IW-ID',
    string: '2024-W03-1',
    typeName: 'PlainDate'
});
// Temporal.PlainDate <2024-01-15>

Missing fields take sensible defaults: month and day default to 1, all time fields and year default to 0, and the time zone to UTC. When a year is written with fewer than four digits, it is adjusted to the value nearest the year 2020, so YY of 95 parses as 1995 and 21 parses as 2021.

PostgreSQL Quirks

The goal of this package isn't to copy the implementation entirely from PostgreSQL. Temporal values are different from Postgres timestamp types so there are intentional functional differences. It reuses the familiar set of patterns so hopefully developers don't have to learn multiple entirely different timestamp templating systems. Because of this, it has also inherited some very specific parsing behavior.

When a format string includes text that is not one of the format patterns, whether quoted or not, it is treated as literal text. By default, literal text in the format string does not need to exactly match the input string. Parsing ignores runs of whitespace in the input string between values. Digit and letter characters will match any one character in the input string. (Any one character, they don't have to be the same character.) Non-digit and non-letter characters will match zero or one non-digit or non-letter characters in the input string. That's a weirdly specific rule, but generally it means that the number of separator characters in the input string must be less than or equal to the number of separator characters in the format string.

_TemporalFormat.fromString({
    format: 'YYYY---MM---DD',
    string: '2026---07---14',
    typeName: 'PlainDate'
});
// This works! The literal text matches exactly.
// Temporal.PlainDate <2026-07-14>

_TemporalFormat.fromString({
    format: 'YYYY---MM---DD',
    string: '2026-07-14',
    typeName: 'PlainDate'
});
// This also works! There are fewer separators in the input string.
// Temporal.PlainDate <2026-07-14>

_TemporalFormat.fromString({
    format: 'YYYY FOO MM BAR DD',
    string: '2026 ABC 07 XYZ 14',
    typeName: 'PlainDate'
});
// This also works! The individual characters don't need to match.
// Temporal.PlainDate <2026-07-14>

_TemporalFormat.fromString({
    format: 'YYYY-MM-DD',
    string: '2026---07---14',
    typeName: 'PlainDate'
});
// This doesn't work! There are fewer separators in the format string.
// throws ParseError

A leading FX switches to fixed-format mode, where literal text is consumed strictly by position. Any non-pattern character in the format string will match any one character in the input string.

_TemporalFormat.fromString({
    format: 'FXYYYY---MM---DD',
    string: '2026---07---14',
    typeName: 'PlainDate'
});
// This works! The literal text matches exactly.
// Temporal.PlainDate <2026-07-14>

_TemporalFormat.fromString({
    format: 'YYYY FOO MM BAR DD',
    string: 'FX2026 ABC 07 XYZ 14',
    typeName: 'PlainDate'
});
// This also works! The individual characters don't need to match.
// Temporal.PlainDate <2026-07-14>

_TemporalFormat.fromString({
    format: 'YYYY---MM---DD',
    string: 'FX2026-07-14',
    typeName: 'PlainDate'
});
// This doesn't work! The number of characters doesn't match.
// throws ParseError

Where PostgreSQL will error if the same pattern is filled twice with different values, TemporalFormat.fromString will allow it but the later value overwrites the former.

Beyond PostgreSQL

Several patterns that PostgreSQL's to_timestamp reads and discards are fully parsed here:

  • J parses a Julian day number into a date: 2451187 parses as 1999-01-08.
  • SSSS and SSSSS parse seconds past midnight and decompose into hour, minute, and second. Explicitly parsed components win: in HH24:MI:SS SSSS, the clock fields take precedence.
  • Time zone names and abbreviations parse (see Time zone parsing).
  • FF7, FF8, FF9, and NS parse fractional seconds to full nanosecond precision.

Strict Mode

When strict mode is enabled, literal text must match exactly.

_TemporalFormat.fromString({
    format: 'YYYY---MM---DD',
    strict: true,
    string: '2026---07---14',
    typeName: 'PlainDate'
});
// This works! The literal text matches exactly.
// Temporal.PlainDate <2026-07-14>

_TemporalFormat.fromString({
    format: 'YYYY FOO MM BAR DD',
    strict: true,
    string: '2026 ABC 07 XYZ 14',
    typeName: 'PlainDate'
});
// This doesn't work! The individual characters don't match.
// throws ParseError

Strict mode takes precedence over the FX modifier.

Time zone parsing

The time zone patterns parse names, abbreviations, and offsets. This goes beyond PostgreSQL's to_timestamp, which reads only numeric offsets.

  • TimeZone, TIMEZONE, and timezone accept IANA identifiers in any case (America/New_York, america/new_york) and canonicalize them, offset identifiers (+05:30, -08), and abbreviations as a fallback.
  • TZ and tz try abbreviations first (PDT, est), then GMT/UTC/UT/Z-relative offsets (GMT+5:30, UTC-8, Z), then identifiers.
  • Abbreviations resolve to fixed offsets, following PostgreSQL's default abbreviation list: PDT is -07:00, EST is -05:00, CST is -06:00 (US Central), and so on. IST is deliberately omitted because it is ambiguous (India, Ireland, and Israel all claim it); use a name or an offset instead. If you need different abbreviation semantics, subclass and override the abbreviation table (see Custom Patterns).
  • A parsed IANA zone becomes the timeZoneId of a ZonedDateTime result, so daylight saving is applied for the parsed wall-clock time. For an Instant result, the zone is used to compute the exact time. If a wall-clock time is ambiguous or skipped at a daylight saving transition, it resolves with Temporal's default 'compatible' disambiguation.
  • The parser reads the longest run of time zone characters that resolves to a zone, so a following literal is still matched: TimeZone"X" reads UTCX as the zone UTC followed by the literal X.
_TemporalFormat.fromString({
    format: 'YYYY-MM-DD HH24:MI:SS TimeZone',
    string: '2026-07-14 12:00:00 America/Los_Angeles',
    typeName: 'ZonedDateTime'
});
// Temporal.ZonedDateTime <2026-07-14T12:00:00-07:00[America/Los_Angeles]>

_TemporalFormat.fromString({
    format: 'YYYY-MM-DD HH24:MI:SS TZ',
    string: '2026-07-14 12:00:00 PDT',
    typeName: 'Instant'
});
// Temporal.Instant <2026-07-14T19:00:00Z>

Parsing limitations

  • D, W, WW, CC, and Q are consumed but do not affect the result. Weekday names are likewise read and then ignored, as in PostgreSQL.
  • For Instant and ZonedDateTime, the time zone defaults to UTC unless a time zone pattern or an offset (TZH, TZM, or OF) is present. A numeric offset produces a fixed-offset zone rather than a named IANA zone.
  • OF reads offset minutes only when they are written after a colon (for example +05:30).
  • A time zone offset should sit immediately after the preceding pattern, as in ISO 8601 output (for example HH24:MI:SSOF). In the default lenient mode, a separator written before the offset is skipped along with the offset's own leading sign, which would drop the sign of a negative offset. The same applies to offset-style values read by the name patterns (for example TimeZone reading +05:30). Keep the offset adjacent, use a named zone, or use FX, so the sign is read correctly.
  • When parsing a Duration, the same numeric and fractional patterns apply with wider digit limits, and the small-year adjustment does not apply: digits are taken literally as a count.

Error Handling

The package throws errors created by isotropic-error.

  • ArgumentError is thrown when value is not a supported Temporal type, when format or string is not a string, or when typeName is not a supported type name.
  • CalendarError is thrown when a value uses a non-ISO calendar (see Calendars).
  • FormatError is thrown when a pattern cannot apply to the value's type (see Type Validation).
  • ParseError is thrown when the input does not match the format, for example when digits, a month name, a meridiem indicator, an era indicator, a Roman numeral, or an offset are expected but not found.
import _TemporalFormat from 'isotropic-temporal-format';

try {
    _TemporalFormat.fromString({
        format: 'YYYY',
        string: 'not a year',
        typeName: 'PlainDate'
    });
} catch (error) {
    // error.name === 'ParseError'
}

Custom Patterns

TemporalFormat is built with isotropic-make, so a subclass can register additional patterns by supplying its own _patternSet. The subclass inherits every built-in pattern; the parent class is not modified.

A pattern object pairs a set of lexemes with format and parse behavior. The base pattern constructors are importable directly: isotropic-temporal-format/lib/number-pattern.js for padded numbers, isotropic-temporal-format/lib/name-pattern.js for names, and so on. For a numeric pattern, configure:

  • lexemeSet: the template lexemes that select the pattern.
  • width: the zero-padded output width.
  • getValue: maps the value's deconstruction (an object with fields like displayYear, month, dayOfMonth, hour, subsecondNs) to the number to render.
  • parseKey: the parsed-field name to assign when reading input (omit to consume and ignore, like PostgreSQL does with CC).
  • requiredCapabilitySet: which value types the pattern applies to (_dateCapabilitySet, _timeCapabilitySet, and so on); anything else raises a FormatError.

This example adds a five-digit year pattern named KYEAR:

import _make from 'isotropic-make';
import _NumberPattern from 'isotropic-temporal-format/lib/number-pattern.js';
import _TemporalFormat from 'isotropic-temporal-format';

const _MyTemporalFormat = _make('MyTemporalFormat', _TemporalFormat, {}, {
    _patternSet: new Set([
        _NumberPattern({
            getValue (deconstruction) {
                return deconstruction.displayYear;
            },
            lexemeSet: new Set([
                'KYEAR'
            ]),
            parseKey: 'year',
            requiredCapabilitySet: _NumberPattern._dateCapabilitySet,
            width: 5
        })
    ])
});

_MyTemporalFormat.toString({
    format: 'KYEAR-MM-DD',
    value: Temporal.PlainDate.from('2024-03-05')
});
// '02024-03-05'

_MyTemporalFormat.fromString({
    format: 'KYEAR-MM-DD',
    string: '02024-03-05',
    typeName: 'PlainDate'
});
// Temporal.PlainDate <2024-03-05>

Because lexing prefers longer lexemes, a custom lexeme can safely contain shorter built-in lexemes (like the Y in KYEAR); the longer match wins.

TemporalFormat uses isotropic-make's object-oriented inheritance patterns but the implementation is entirely static. TemporalFormat itself is a constructor function but there is no reason to ever create an instance of it.

Contributing

Please refer to CONTRIBUTING.md for information on how to contribute to this project.

Issues

If you encounter any issues, please file them at https://github.com/ibi-group/isotropic-temporal-format/issues