moon_edtf

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

    edtf
    datetime
    dates
    uncertainty
    diagnostics
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    20 hours ago
    Downloads
    3

    #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

    After publication is confirmed, add yyqdbngt/moon_edtf@0.1.0 and import it in your moon.pkg. Source-based verification is documented in the repository README.

    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

    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. Serialization preserves the mask instead of inventing 1990 or 1900.
    • 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.
    • Batch diagnostics with stable error codes.

    Month and day boundaries are validated. For exact years, 02-29 uses the proleptic Gregorian leap-year rule. For masked years the leap-year status is unknown, so February 29 is allowed; February 30 and April 31 are still rejected.

    #Explicit non-goals

    No natural-language date recognition. No complete calendar/timeline expansion. No conversion of uncertain/approximate/masked values into precise timestamps. No time/zone syntax, masked month/day, nested sets, open/unknown set endpoints, individual prefix qualifiers, curly-brace all-member sets 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.

    DaySpec

    pub(all) enum DaySpec {
    Unspecified
    Exact(Int)
    } 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.

    EdtfInterval

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

    EdtfValue

    pub(all) enum EdtfValue {
    Date(EdtfDate)
    Interval(EdtfInterval)
    Set(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)
    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.

    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.

    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.

    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.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io