moon_edtf

    Conservative pure-MoonBit EDTF subset parser that preserves uncertainty and rejects guesswork.

    edtf
    datetime
    dates
    uncertainty
    diagnostics
    Download zip
    Author
    Version
    0.2.0
    License
    Apache-2.0
    Last updated
    last month
    Downloads
    37

    No dependencies

    #Moon EDTF

    Pure-MoonBit parser for a conservative EDTF subset. It preserves uncertainty, approximation, masked low-order digits, and open/unknown interval endpoints. Zero third-party package dependencies.

    #Consumer example

    The published MoonCakes version is 0.1.0; this repository's 0.2.0 is pending CI and publication. To use the new APIs before release, build from source. After release, add yyqdbngt/moon_edtf@0.2.0 and import it in moon.pkg.

    import { "yyqdbngt/moon_edtf" @edtf, }

    ///|
    fn main {
    let value = try! @edtf.parse("1984?")
    println(value.to_string())
    let rows = @edtf.diagnose_batch(["1984?", "1984-13"])
    println(rows[0].normalized)
    }

    The source repository contains a runnable version in examples/basic.

    #Public API

    • parse(input : String) -> EdtfValue raise EdtfError
    • EdtfValue::to_string(self) -> String
    • normalize(value : EdtfValue) -> String
    • diagnose(input : String) -> Diagnostic
    • diagnose_batch(inputs : Array[String]) -> Array[Diagnostic]
    • compare(left : EdtfValue, right : EdtfValue) -> Int raise EdtfError
    • date_envelope(value : EdtfValue) -> DateEnvelope raise EdtfError
    • window_relation(value, query_start, query_end) -> WindowRelation raise EdtfError
    • audit_catalog(entries, query_start, query_end) -> CatalogReport raise EdtfError

    EdtfError is a stable error type with variants Syntax(code, offset) and Unsupported(code, offset). offset is a half-open UTF-16 offset into the original input. Error values do not echo input text.

    Diagnostic contains input, valid, error_code, error_offset, and normalized. Valid rows set error_code to "" and error_offset to -1. Invalid rows keep normalized as "".

    #Supported EDTF subset

    • Exact dates: YYYY, YYYY-MM, YYYY-MM-DD, negative years, and years with more than four digits with a mandatory Y prefix (Y10000, Y-10000). Exact years are limited to nine decimal digits to fit Int.
    • Seasons: YYYY-21 through YYYY-24 as a documented extension. A season has no day component.
    • Qualifiers: ? (uncertain), ~ (approximate), % (both). Qualifiers may appear as suffixes and apply to that component and all components to its left. Fields store effective qualifications; normalization may remove redundant markers.
    • Masked low-order year digits: 199X, 19XX, in year-only expressions. Unspecified month/day shapes include 2004-XX, 1985-04-XX, and 1985-XX-XX. Serialization preserves missing precision.
    • Complete date-times with HH:MM:SS and local, Z, hour-only, or hour-minute offsets. Zone spelling is preserved, not converted.
    • Intervals: start/end; Endpoint::Open is .., while Endpoint::Unknown is an empty side. The distinction is preserved. Bare .. is rejected.
    • Choice sets: [1667,1668] and [1667,1668,1670..1672]. A set-local a..b range remains a..b and is stored as EdtfValue::Range, not Interval.
    • All-member sets use {...} and remain distinct from one-of [...] sets.
    • Batch diagnostics with stable error codes.
    • Conservative date envelopes and inclusive exact-day window screening. Qualified dates, seasons, unbounded intervals and date-times require review; PossibleOverlap can be a false positive where an envelope has gaps.
    • Structured catalog batch audit with invalid/review/outside/within/possible findings and counts. See examples/catalog-audit in the repository.

    Month and day boundaries are validated. Exact years use the proleptic Gregorian leap-year rule. Masked years are year-only; 19XX-02-29 is rejected in this subset.

    #Explicit non-goals

    No natural-language date recognition. No complete calendar/timeline expansion. No conversion of uncertain/approximate/masked values into precise timestamps. No date-time fractions, leap seconds, 24:00, timezone conversion, nested sets, open/unknown set endpoints, individual prefix qualifiers or slash intervals inside choice sets. No complete EDTF conformance level is claimed, including Level 0.

    The round-trip contract applies to parser-produced values. Public constructors can represent invalid combinations and are not a substitute for parse validation.

    #Comparison contract

    compare only orders two complete, unqualified exact dates (YYYY-MM-DD). Qualified, partial, masked, interval, set, open, and unknown values raise Unsupported with a stable code; they are never coerced into a fake precise order.

    #License

    Apache-2.0. See repository docs/provenance.md for EDTF sources, ecosystem overlap findings, and AI-assisted development disclosure.

    EdtfError

    pub(all) suberror EdtfError {
    Syntax(String, Int)
    Unsupported(String, Int)
    } derive(Eq,
    Debug
    )

    Stable public error surface. Every failure is a code plus a half-open UTF-16 offset into the original input. Error values never echo input text.

    CalendarDay

    pub(all) struct CalendarDay {
    year : Int
    month : Int
    day : Int
    } derive(Eq,
    Debug
    )

    One exact calendar day. This type is used only as a conservative bound; it must not be confused with the precision of the source EDTF value.

    CalendarDay::to_string

    fn CalendarDay::to_string(self : CalendarDay) -> String

    CatalogDecision

    pub(all) enum CatalogDecision {
    Invalid
    NeedsReview
    OutsideWindow
    WithinWindow
    PossibleOverlapWindow
    } derive(Eq,
    Debug
    )

    A row may be invalid syntax, require a human judgment, or have a safe envelope relationship to the requested exact-day window.

    CatalogDecision::to_string

    fn CatalogDecision::to_string(self : CatalogDecision) -> String

    CatalogEntry

    pub(all) struct CatalogEntry {
    record_id : String
    edtf : String
    } derive(Eq,
    Debug
    )

    One structured date field supplied by an ingest pipeline. The library deliberately does not read CSV, MARC, JSON or network data itself.

    CatalogFinding

    pub(all) struct CatalogFinding {
    record_id : String
    input : String
    normalized : String
    decision : CatalogDecision
    error_code : String
    error_offset : Int
    envelope : DateEnvelope?
    } derive(Eq,
    Debug
    )

    An Invalid row has error_offset in the original UTF-16 input; NeedsReview carries an unsupported-operation code with offset -1. Rows with a window decision have a conservative envelope but no error.

    CatalogReport

    pub(all) struct CatalogReport {
    findings : Array[CatalogFinding]
    invalid : Int
    needs_review : Int
    outside : Int
    within : Int
    possible_overlap : Int
    } derive(Eq,
    Debug
    )

    DateEnvelope

    pub(all) struct DateEnvelope {
    earliest : CalendarDay
    latest : CalendarDay
    } derive(Eq,
    Debug
    )

    Inclusive lower and upper bounds of an unqualified date or closed interval. An envelope can include days that the source expression does not actually denote; callers must not treat it as a complete date expansion.

    DaySpec

    pub(all) enum DaySpec {
    Unspecified
    Exact(Int)
    Masked
    } derive(Eq,
    Debug
    )

    Diagnostic

    pub(all) struct Diagnostic {
    input : String
    valid : Bool
    error_code : String
    error_offset : Int
    normalized : String
    } derive(Eq,
    Debug
    )

    Batch diagnostic record. Valid rows set error_code to the empty string and error_offset to -1; invalid rows keep normalized empty.

    EdtfDate

    pub(all) struct EdtfDate {
    year : YearSpec
    year_qualifier : Qualifier
    month : MonthSpec
    month_qualifier : Qualifier
    day : DaySpec
    day_qualifier : Qualifier
    } derive(Eq,
    Debug
    )

    A parsed EDTF date. Precision is explicit: absent month/day components are represented as Unspecified, never as fake 01 values.

    EdtfDateTime

    pub(all) struct EdtfDateTime {
    date : EdtfDate
    hour : Int
    minute : Int
    second : Int
    zone : TimeZone
    } derive(Eq,
    Debug
    )

    A complete calendar date followed by HH:MM:SS and an optional zone. Fractions, leap seconds and 24:00 are outside this documented subset.

    EdtfInterval

    pub(all) struct EdtfInterval {
    start : Endpoint
    end : Endpoint
    } derive(Eq,
    Debug
    )

    EdtfValue

    pub(all) enum EdtfValue {
    Date(EdtfDate)
    DateTime(EdtfDateTime)
    Interval(EdtfInterval)
    Set(Array[EdtfValue])
    AllSet(Array[EdtfValue])
    Range(EdtfDate, EdtfDate)
    } derive(Eq,
    Debug
    )

    The public value union for the supported EDTF subset.

    EdtfValue::to_string

    fn EdtfValue::to_string(self : EdtfValue) -> String

    Return a normalized EDTF spelling. Normalization never promotes an uncertain/approximate/masked value to a precise timestamp.

    Endpoint

    pub(all) enum Endpoint {
    Known(EdtfDate)
    Open
    Unknown
    } derive(Eq,
    Debug
    )

    One interval endpoint: .. is Open; an empty side is Unknown.

    MonthSpec

    pub(all) enum MonthSpec {
    Unspecified
    Exact(Int)
    Masked
    Season(Int)
    } derive(Eq,
    Debug
    )

    Qualifier

    pub(all) struct Qualifier {
    uncertain : Bool
    approximate : Bool
    } derive(Eq,
    Debug
    )

    Effective uncertainty and approximation of one date component.

    Qualifier::none

    fn Qualifier::none() -> Qualifier

    No marker.

    TimeZone

    pub(all) enum TimeZone {
    Local
    Utc
    Offset(Int, Int, Int, Bool)
    } derive(Eq,
    Debug
    )

    The explicit zone spelling is retained. Local, UTC, hour-only shifts and hour-minute shifts are distinct; no implicit host-timezone conversion occurs.

    WindowRelation

    pub(all) enum WindowRelation {
    Outside
    Within
    PossibleOverlap
    } derive(Eq,
    Debug
    )

    YearSpec

    pub(all) enum YearSpec {
    Exact(Int)
    Masked(String, Int)
    } derive(Eq,
    Debug
    )

    An exact year, or a year whose least-significant digits are unspecified. Masked keeps the written digit prefix and the number of trailing X positions so 19XX never becomes a fake 1900.

    audit_catalog

    fn audit_catalog(entries : Array[CatalogEntry], query_start : String, query_end : String) -> CatalogReport raise EdtfError

    Validate and screen a catalog batch against an inclusive exact-day window. Invalid date fields are not silently dropped; uncertainty, approximation, seasons, sets and open/unknown intervals go to NeedsReview rather than receiving fabricated bounds. PossibleOverlapWindow is deliberately conservative and can include false positives; it is not an exact match. An invalid query window raises before any row is examined.

    compare

    fn compare(left : EdtfValue, right : EdtfValue) -> Int raise EdtfError

    Compare only complete, unqualified exact dates. Qualified, partial, masked, open, unknown, set, and interval values raise Unsupported instead of pretending to have a precise order.

    date_envelope

    fn date_envelope(value : EdtfValue) -> DateEnvelope raise EdtfError

    Give a safe inclusive envelope for a parser-produced date or closed interval. Masked digits widen the bound; missing components widen it to their entire calendar period. Qualified dates, seasons, open/unknown endpoints are refused rather than guessed. Sets and ranges receive only their outer envelope, which can include gaps with no actual member.

    diagnose

    fn diagnose(input : String) -> Diagnostic

    Return a single diagnostic record. Invalid rows carry a stable error code and the original UTF-16 offset; valid rows carry the normalized EDTF string.

    diagnose_batch

    fn diagnose_batch(inputs : Array[String]) -> Array[Diagnostic]

    Batch-diagnose an array of EDTF candidate strings without throwing.

    normalize

    fn normalize(value : EdtfValue) -> String

    Convenience alias for value.to_string().

    parse

    fn parse(input : String) -> EdtfValue raise EdtfError

    Parse a strict EDTF subset. Raises EdtfError::Syntax(code, offset) on invalid input and never guesses missing components or uncertain markers.

    window_relation

    fn window_relation(value : EdtfValue, query_start : String, query_end : String) -> WindowRelation raise EdtfError

    Classify an EDTF value against an inclusive exact-day search window. PossibleOverlap means only the conservative envelope intersects, not that a particular day is known to occur. The query endpoints must each be a complete, unqualified YYYY-MM-DD EDTF date.