diff --git a/README.md b/README.md index 7393db1..89d5477 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Duper excels in a variety of use cases: - In configuration files, where users are expected to swap out values, its explicit types can be a helpful guide. - Thanks to its self-documenting identifiers, Duper feels right at home in REST APIs. -- With support for bytes and raw data, minimal syntax, and compatibility with JSON, Duper is also a great choice for data interchange. +- With a simple syntax and extended type support, using Duper for logs is a breath of fresh air for both manual and tool-assisted debugging. ## For implementers diff --git a/duper-python/pyproject.toml b/duper-python/pyproject.toml index 85e527b..2cdf839 100644 --- a/duper-python/pyproject.toml +++ b/duper-python/pyproject.toml @@ -16,7 +16,9 @@ classifiers = [ "Framework :: Pydantic", "Framework :: Pydantic :: 2", ] -dependencies = ["pydantic>=2,<3"] +dependencies = [ + "pydantic>=2,<3", +] [project.urls] homepage = "https://duper.dev.br" diff --git a/duper-python/src/de.rs b/duper-python/src/de.rs index 625ab4d..d52e8a4 100644 --- a/duper-python/src/de.rs +++ b/duper-python/src/de.rs @@ -1,4 +1,4 @@ -use duper::visitor::DuperVisitor; +use duper::{DuperTemporal, visitor::DuperVisitor}; use pyo3::{prelude::*, types::*}; use crate::Duper; @@ -124,11 +124,62 @@ impl<'py> DuperVisitor for Visitor<'py> { fn visit_temporal<'a>( &mut self, identifier: Option<&duper::DuperIdentifier<'a>>, - temporal: &duper::DuperTemporal<'a>, + temporal: &DuperTemporal<'a>, ) -> Self::Value { - // TO-DO: Temporal - Use the appropriate temporal-python type for the DuperTemporal value Ok(VisitorValue { - value: PyString::new(self.py, &temporal.clone().into_inner()).into_any(), + value: match temporal { + DuperTemporal::Instant(inner) => self + .py + .import("duper.temporal")? + .getattr("Instant")? + .getattr("from_string")? + .call1((PyString::new(self.py, inner.as_ref()),))?, + DuperTemporal::ZonedDateTime(inner) => self + .py + .import("duper.temporal")? + .getattr("ZonedDateTime")? + .getattr("from_string")? + .call1((PyString::new(self.py, inner.as_ref()),))?, + DuperTemporal::PlainDate(inner) => self + .py + .import("duper.temporal")? + .getattr("PlainDate")? + .getattr("from_string")? + .call1((PyString::new(self.py, inner.as_ref()),))?, + DuperTemporal::PlainTime(inner) => self + .py + .import("duper.temporal")? + .getattr("PlainTime")? + .getattr("from_string")? + .call1((PyString::new(self.py, inner.as_ref()),))?, + DuperTemporal::PlainDateTime(inner) => self + .py + .import("duper.temporal")? + .getattr("PlainDateTime")? + .getattr("from_string")? + .call1((PyString::new(self.py, inner.as_ref()),))?, + DuperTemporal::PlainYearMonth(inner) => self + .py + .import("duper.temporal")? + .getattr("PlainYearMonth")? + .getattr("from_string")? + .call1((PyString::new(self.py, inner.as_ref()),))?, + DuperTemporal::PlainMonthDay(inner) => self + .py + .import("duper.temporal")? + .getattr("PlainMonthDay")? + .getattr("from_string")? + .call1((PyString::new(self.py, inner.as_ref()),))?, + DuperTemporal::Duration(inner) => self + .py + .import("duper.temporal")? + .getattr("Duration")? + .getattr("from_string")? + .call1((PyString::new(self.py, inner.as_ref()),))?, + DuperTemporal::Unspecified(inner) => { + PyString::new(self.py, inner.as_ref()).into_any() + } + }, duper: identifier .map(|identifier| Duper::from_identifier(identifier)?.into_pyobject(self.py)) .transpose()?, diff --git a/duper-python/src/ser/well_known_type.rs b/duper-python/src/ser/well_known_type.rs index 07582bc..d65a51b 100644 --- a/duper-python/src/ser/well_known_type.rs +++ b/duper-python/src/ser/well_known_type.rs @@ -90,6 +90,15 @@ pub(crate) enum WellKnownType<'py> { Ulid(Bound<'py, PyAny>), // re Pattern(Bound<'py, PyAny>), + // temporal + TemporalInstant(Bound<'py, PyAny>), + TemporalZonedDateTime(Bound<'py, PyAny>), + TemporalPlainDate(Bound<'py, PyAny>), + TemporalPlainTime(Bound<'py, PyAny>), + TemporalPlainDateTime(Bound<'py, PyAny>), + TemporalPlainYearMonth(Bound<'py, PyAny>), + TemporalPlainMonthDay(Bound<'py, PyAny>), + TemporalDuration(Bound<'py, PyAny>), // uuid Uuid(Bound<'py, PyAny>), } @@ -298,6 +307,31 @@ impl<'py> WellKnownType<'py> { } // re ("re", "Pattern") => return Ok(Some(WellKnownType::Pattern(value.clone()))), + // temporal + ("duper.temporal", "Instant") => { + return Ok(Some(WellKnownType::TemporalInstant(value.clone()))); + } + ("duper.temporal", "ZonedDateTime") => { + return Ok(Some(WellKnownType::TemporalZonedDateTime(value.clone()))); + } + ("duper.temporal", "PlainDate") => { + return Ok(Some(WellKnownType::TemporalPlainDate(value.clone()))); + } + ("duper.temporal", "PlainTime") => { + return Ok(Some(WellKnownType::TemporalPlainTime(value.clone()))); + } + ("duper.temporal", "PlainDateTime") => { + return Ok(Some(WellKnownType::TemporalPlainDateTime(value.clone()))); + } + ("duper.temporal", "PlainYearMonth") => { + return Ok(Some(WellKnownType::TemporalPlainYearMonth(value.clone()))); + } + ("duper.temporal", "PlainMonthDay") => { + return Ok(Some(WellKnownType::TemporalPlainMonthDay(value.clone()))); + } + ("duper.temporal", "Duration") => { + return Ok(Some(WellKnownType::TemporalDuration(value.clone()))); + } // uuid ("uuid", "UUID") => return Ok(Some(WellKnownType::Uuid(value.clone()))), _ => (), @@ -341,29 +375,71 @@ impl<'py> WellKnownType<'py> { inner: serialize_pyany(value.getattr("__dict__")?)?.inner, }), // datetime - WellKnownType::DateTime(value) => Ok(DuperValue { - identifier: Some( - DuperIdentifier::try_from(Cow::Borrowed("DateTime")).expect("valid identifier"), - ), - inner: DuperInner::Temporal( - DuperTemporal::try_instant_from(Cow::Owned( - value.call_method0("isoformat")?.extract()?, - )) - .map_err(|err| { - PyValueError::new_err(format!("Failed to parse Temporal value: {err}")) - })?, - ), + WellKnownType::DateTime(value) => Ok({ + let datetime: Cow<'_, str> = + Cow::Owned(value.call_method0("isoformat")?.extract()?); + let tzinfo = value.getattr("tzinfo")?; + if tzinfo.is_none() || tzinfo.getattr("utcoffset")?.call1((value,))?.is_none() { + DuperValue { + identifier: Some( + DuperIdentifier::try_from(Cow::Borrowed("PlainDateTime")) + .expect("valid identifier"), + ), + inner: DuperInner::Temporal( + DuperTemporal::try_plain_date_time_from(datetime).map_err(|err| { + PyValueError::new_err(format!( + "Failed to parse Temporal value: {err}" + )) + })?, + ), + } + } else { + DuperValue { + identifier: Some( + DuperIdentifier::try_from(Cow::Borrowed("Instant")) + .expect("valid identifier"), + ), + inner: DuperInner::Temporal( + DuperTemporal::try_instant_from(datetime).map_err(|err| { + PyValueError::new_err(format!( + "Failed to parse Temporal value: {err}" + )) + })?, + ), + } + } }), WellKnownType::TimeDelta(value) => Ok(DuperValue { identifier: Some( - DuperIdentifier::try_from(Cow::Borrowed("TimeDelta")) - .expect("valid identifier"), - ), - inner: DuperInner::String(DuperString::from(Cow::Owned(value.str()?.extract()?))), + DuperIdentifier::try_from(Cow::Borrowed("Duration")).expect("valid identifier"), + ), + inner: { + let py = value.py(); + let datetime_module = py.import("datetime")?; + let pydantic_module = py.import("pydantic")?; + let adapter = pydantic_module + .getattr("TypeAdapter")? + .call1((datetime_module.getattr("timedelta")?,))?; + let kwargs = PyDict::new(py); + kwargs.set_item("mode", "json")?; + let dump = adapter + .getattr("dump_python")? + .call((value,), Some(&kwargs))?; + DuperInner::Temporal( + DuperTemporal::try_duration_from(Cow::Owned(dump.extract()?)).map_err( + |err| { + PyValueError::new_err(format!( + "Failed to parse Temporal value: {err}" + )) + }, + )?, + ) + }, }), WellKnownType::Date(value) => Ok(DuperValue { identifier: Some( - DuperIdentifier::try_from(Cow::Borrowed("Date")).expect("valid identifier"), + DuperIdentifier::try_from(Cow::Borrowed("PlainDate")) + .expect("valid identifier"), ), inner: DuperInner::Temporal( DuperTemporal::try_plain_date_from(Cow::Owned( @@ -376,7 +452,8 @@ impl<'py> WellKnownType<'py> { }), WellKnownType::Time(value) => Ok(DuperValue { identifier: Some( - DuperIdentifier::try_from(Cow::Borrowed("Time")).expect("valid identifier"), + DuperIdentifier::try_from(Cow::Borrowed("PlainTime")) + .expect("valid identifier"), ), inner: DuperInner::Temporal( DuperTemporal::try_plain_time_from(Cow::Owned( @@ -767,6 +844,103 @@ impl<'py> WellKnownType<'py> { value.getattr("pattern")?.extract()?, ))), }), + // temporal + WellKnownType::TemporalInstant(value) => Ok(DuperValue { + identifier: Some(DuperIdentifier::try_from("Instant").expect("valid identifier")), + inner: DuperInner::Temporal( + DuperTemporal::try_instant_from(Cow::Owned( + value.getattr("to_string")?.call0()?.extract()?, + )) + .map_err(|err| { + PyValueError::new_err(format!("Failed to parse Instant: {err}")) + })?, + ), + }), + WellKnownType::TemporalZonedDateTime(value) => Ok(DuperValue { + identifier: Some( + DuperIdentifier::try_from("ZonedDateTime").expect("valid identifier"), + ), + inner: DuperInner::Temporal( + DuperTemporal::try_zoned_date_time_from(Cow::Owned( + value.getattr("to_string")?.call0()?.extract()?, + )) + .map_err(|err| { + PyValueError::new_err(format!("Failed to parse ZonedDateTime: {err}")) + })?, + ), + }), + WellKnownType::TemporalPlainDate(value) => Ok(DuperValue { + identifier: Some(DuperIdentifier::try_from("PlainDate").expect("valid identifier")), + inner: DuperInner::Temporal( + DuperTemporal::try_plain_date_from(Cow::Owned( + value.getattr("to_string")?.call0()?.extract()?, + )) + .map_err(|err| { + PyValueError::new_err(format!("Failed to parse PlainDate: {err}")) + })?, + ), + }), + WellKnownType::TemporalPlainTime(value) => Ok(DuperValue { + identifier: Some(DuperIdentifier::try_from("PlainTime").expect("valid identifier")), + inner: DuperInner::Temporal( + DuperTemporal::try_plain_time_from(Cow::Owned( + value.getattr("to_string")?.call0()?.extract()?, + )) + .map_err(|err| { + PyValueError::new_err(format!("Failed to parse PlainTime: {err}")) + })?, + ), + }), + WellKnownType::TemporalPlainDateTime(value) => Ok(DuperValue { + identifier: Some( + DuperIdentifier::try_from("PlainDateTime").expect("valid identifier"), + ), + inner: DuperInner::Temporal( + DuperTemporal::try_plain_date_time_from(Cow::Owned( + value.getattr("to_string")?.call0()?.extract()?, + )) + .map_err(|err| { + PyValueError::new_err(format!("Failed to parse PlainDateTime: {err}")) + })?, + ), + }), + WellKnownType::TemporalPlainYearMonth(value) => Ok(DuperValue { + identifier: Some( + DuperIdentifier::try_from("PlainYearMonth").expect("valid identifier"), + ), + inner: DuperInner::Temporal( + DuperTemporal::try_plain_year_month_from(Cow::Owned( + value.getattr("to_string")?.call0()?.extract()?, + )) + .map_err(|err| { + PyValueError::new_err(format!("Failed to parse PlainYearMonth: {err}")) + })?, + ), + }), + WellKnownType::TemporalPlainMonthDay(value) => Ok(DuperValue { + identifier: Some( + DuperIdentifier::try_from("PlainMonthDay").expect("valid identifier"), + ), + inner: DuperInner::Temporal( + DuperTemporal::try_plain_month_day_from(Cow::Owned( + value.getattr("to_string")?.call0()?.extract()?, + )) + .map_err(|err| { + PyValueError::new_err(format!("Failed to parse PlainMonthDay: {err}")) + })?, + ), + }), + WellKnownType::TemporalDuration(value) => Ok(DuperValue { + identifier: Some(DuperIdentifier::try_from("Duration").expect("valid identifier")), + inner: DuperInner::Temporal( + DuperTemporal::try_duration_from(Cow::Owned( + value.getattr("to_string")?.call0()?.extract()?, + )) + .map_err(|err| { + PyValueError::new_err(format!("Failed to parse Duration: {err}")) + })?, + ), + }), // uuid WellKnownType::Uuid(value) => Ok(DuperValue { identifier: Some( diff --git a/duper-python/tests/test_basic.py b/duper-python/tests/test_basic.py index 1de5987..e16f998 100644 --- a/duper-python/tests/test_basic.py +++ b/duper-python/tests/test_basic.py @@ -2,7 +2,8 @@ import pytest import duper -DUPER_DATA = """APIResponse({ +DUPER_DATA = """ +APIResponse({ status: 200, headers: { content_type: "application/duper", @@ -41,7 +42,7 @@ def test_basic(): assert ( duper.dumps(obj, strip_identifiers=True) - == r"""{status: 200, headers: {content_type: "application/duper", cache_control: "max-age=3600"}, body: {users: [{id: "7039311b-02d2-4849-a6de-900d4dbe9acb", name: "Alice", email: "alice@example.com", roles: ["admin", "user"], metadata: {last_login: "2024-01-15T10:30:00Z", ip: "173.255.230.79"}}]}}""" + == r"""{status: 200, headers: {content_type: "application/duper", cache_control: "max-age=3600"}, body: {users: [{id: "7039311b-02d2-4849-a6de-900d4dbe9acb", name: "Alice", email: "alice@example.com", roles: ["admin", "user"], metadata: {last_login: '2024-01-15T10:30:00Z', ip: "173.255.230.79"}}]}}""" ) assert ( @@ -51,7 +52,7 @@ def test_basic(): assert ( duper.dumps(obj, strip_identifiers=True, minify=True) - == r"""{status:200,headers:{content_type:"application/duper",cache_control:"max-age=3600"},body:{users:[{id:"7039311b-02d2-4849-a6de-900d4dbe9acb",name:"Alice",email:"alice@example.com",roles:["admin","user"],metadata:{last_login:"2024-01-15T10:30:00Z",ip:"173.255.230.79"}}]}}""" + == r"""{status:200,headers:{content_type:"application/duper",cache_control:"max-age=3600"},body:{users:[{id:"7039311b-02d2-4849-a6de-900d4dbe9acb",name:"Alice",email:"alice@example.com",roles:["admin","user"],metadata:{last_login:'2024-01-15T10:30:00Z',ip:"173.255.230.79"}}]}}""" ) with pytest.raises( diff --git a/duper-python/tests/test_pydantic.py b/duper-python/tests/test_pydantic.py index c9a68a9..3d56b92 100644 --- a/duper-python/tests/test_pydantic.py +++ b/duper-python/tests/test_pydantic.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from collections import deque -from datetime import datetime +from datetime import datetime, timedelta from decimal import Decimal from enum import Enum from ipaddress import ( @@ -75,11 +75,13 @@ def test_pydantic_complex(): @dataclass class Regex: - pattern: re.Pattern + pattern: re.Pattern[str] matches: list[str] | None = None class Complex(BaseModel): datetime: datetime + duration: timedelta + # zdt: ZonedDateTime uuid: UUID deque: deque[str] named_tuple: MyTuple @@ -94,6 +96,10 @@ def test_pydantic_complex(): val = Complex( datetime="2025-10-12T20:01:28.400086", + duration=timedelta(days=7, seconds=5, microseconds=1), + # zdt=ZonedDateTime.from_string( + # "2022-02-28T11:06:00.092121729+08:00[Asia/Shanghai][u-ca=chinese]" + # ), uuid="a708f86d-ee5b-4ce8-b505-8f59d3d26850", deque=deque(), named_tuple=(34, 35), @@ -118,7 +124,7 @@ def test_pydantic_complex(): assert ( val_dump - == """Complex({datetime: DateTime('2025-10-12T20:01:28.400086'), uuid: Uuid("a708f86d-ee5b-4ce8-b505-8f59d3d26850"), deque: Deque([]), named_tuple: (34, 35), set: Set([1, 2, 4]), bytesize: ByteSize(3072000), decimal: Decimal("12.34"), enum: IPv4Address(2), typeddict: {x: 1, y: 2, label: "good"}, path: PosixPath("/dev/null"), regex: Regex({pattern: Pattern("^Hello w.rld!$"), matches: null}), sub: Submodel({address4: IPv4Address("192.168.0.1"), interface4: IPv4Interface("192.168.0.2/32"), network4: IPv4Network("192.168.0.0/24"), address6: IPv6Address("2001:db8::1"), interface6: IPv6Interface("2001:db8::2/128"), network6: IPv6Network("2001:db8::/128")})})""" + == """Complex({datetime: PlainDateTime('2025-10-12T20:01:28.400086'), duration: Duration('P7DT5.000001S'), zdt: ZonedDateTime('2022-02-28T11:06:00.092121729+08:00[Asia/Shanghai][u-ca=chinese]'), uuid: Uuid("a708f86d-ee5b-4ce8-b505-8f59d3d26850"), deque: Deque([]), named_tuple: (34, 35), set: Set([1, 2, 4]), bytesize: ByteSize(3072000), decimal: Decimal("12.34"), enum: IPv4Address(2), typeddict: {x: 1, y: 2, label: "good"}, path: PosixPath("/dev/null"), regex: Regex({pattern: Pattern("^Hello w.rld!$"), matches: null}), sub: Submodel({address4: IPv4Address("192.168.0.1"), interface4: IPv4Interface("192.168.0.2/32"), network4: IPv4Network("192.168.0.0/24"), address6: IPv6Address("2001:db8::1"), interface6: IPv6Interface("2001:db8::2/128"), network6: IPv6Network("2001:db8::/128")})})""" ) val2 = Complex.model_validate_duper(val_dump) diff --git a/duper-vs-code/syntaxes/duper.tmLanguage.json b/duper-vs-code/syntaxes/duper.tmLanguage.json index f74d654..4c7c406 100644 --- a/duper-vs-code/syntaxes/duper.tmLanguage.json +++ b/duper-vs-code/syntaxes/duper.tmLanguage.json @@ -23,7 +23,7 @@ "strings": { "patterns": [ { - "name": "string.quoted.double.raw_one.duper", + "name": "string.quoted.double.raw.duper", "begin": "b?r(#*)\"", "beginCaptures": { "1": { @@ -37,6 +37,17 @@ } } }, + { + "name": "string.quoted.base64.duper", + "begin": "b64\"", + "end": "\"", + "patterns": [ + { + "name": "string.quoted.base64content.duper", + "match": "\\s*[A-Za-z0-9+/=]+\\s*" + } + ] + }, { "name": "string.quoted.double.duper", "begin": "b?\"", @@ -46,17 +57,21 @@ "include": "#stringescapes" }, { - "include": "#stringcontent" + "match": ".", + "name": "string.quoted.content.duper" } ] - } - ] - }, - "stringcontent": { - "patterns": [ + }, { - "match": ".", - "name": "string.quoted.double.duper" + "name": "string.single.temporal.duper", + "begin": "'", + "end": "'", + "patterns": [ + { + "match": ".", + "name": "string.single.content.duper" + } + ] } ] }, diff --git a/duper/src/format.rs b/duper/src/format.rs index 7036f49..0361bc7 100644 --- a/duper/src/format.rs +++ b/duper/src/format.rs @@ -159,44 +159,11 @@ pub(crate) fn format_duper_bytes<'a>(bytes: &'a DuperBytes<'a>) -> Cow<'a, str> } pub(crate) fn format_temporal<'a>(temporal: &'a DuperTemporal<'a>) -> String { - match temporal { - DuperTemporal::Instant(inner) => { - let value = inner.as_ref(); - format!("Instant('{value}')") - } - DuperTemporal::ZonedDateTime(inner) => { - let value = inner.as_ref(); - format!("ZonedDateTime('{value}')") - } - DuperTemporal::PlainDate(inner) => { - let value = inner.as_ref(); - format!("PlainDate('{value}')") - } - DuperTemporal::PlainTime(inner) => { - let value = inner.as_ref(); - format!("PlainTime('{value}')") - } - DuperTemporal::PlainDateTime(inner) => { - let value = inner.as_ref(); - format!("PlainDateTime('{value}')") - } - DuperTemporal::PlainYearMonth(inner) => { - let value = inner.as_ref(); - format!("PlainYearMonth('{value}')") - } - DuperTemporal::PlainMonthDay(inner) => { - let value = inner.as_ref(); - format!("PlainMonthDay('{value}')") - } - DuperTemporal::Duration(inner) => { - let value = inner.as_ref(); - format!("Duration('{value}')") - } - DuperTemporal::Unspecified(inner) => { - let value = inner.as_ref(); - format!("'{value}'") - } - } + let mut string = String::with_capacity(temporal.as_ref().len() + 2); + string.push('\''); + string.push_str(temporal.as_ref()); + string.push('\''); + string } pub(crate) fn format_integer(integer: i64) -> String { diff --git a/duper_website/docs/index.md b/duper_website/docs/index.md index 5d7a6e8..314dd47 100644 --- a/duper_website/docs/index.md +++ b/duper_website/docs/index.md @@ -57,9 +57,9 @@ const initial = `UserProfile({ Duper excels in a variety of use cases: -- **Configuration files**: Duper's explicit types and identifiers serve as helpful guides when users need to modify values. +- **Configuration files**: Duper's explicit types and comments serve as helpful guides when users need to modify values. - **REST APIs**: Self-documenting identifiers make Duper a natural fit for API responses. -- **Data interchange**: With support for bytes, raw data, JSON compatibility, and identifiers that can be turned on for debugging-only, Duper is ideal for data exchange between systems. +- **Logging**: With a simple syntax and extended type support, Duper is a breath of fresh air for both manual and tool-assisted debugging. ## Playground @@ -79,7 +79,7 @@ Duper excels in a variety of use cases: | Raw strings | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | | Identifiers/tags | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | | Unambiguous | ✅ | ✅ | ✅ | ❌\[3] | ⚠️\[4] | ✅ | -| Simple | ⚠️\[2] | ✅ | ✅ | ❌ | ✅ | ✅ | +| Simple | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | | JSON-compatible | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | | Popular | ❌ | ✅ | ⚠️ | ✅ | ✅ | ⚠️ | diff --git a/duper_website/docs/quick-start.md b/duper_website/docs/quick-start.md index b916ffd..5d3678e 100644 --- a/duper_website/docs/quick-start.md +++ b/duper_website/docs/quick-start.md @@ -1,6 +1,6 @@ # Quick start -## JavaScript +## JavaScript (WebAssembly) ```bash npm install --save @duper-js/wasm diff --git a/duper_website/docs/spec.md b/duper_website/docs/spec.md index d73fe26..a2fca85 100644 --- a/duper_website/docs/spec.md +++ b/duper_website/docs/spec.md @@ -269,13 +269,22 @@ Byte strings are similar to strings, but represent binary data. Like strings, th ## Temporal values -Temporal values are a set of value types, representing a point in time or the difference between two points in time. They are surrounded by single quotes `'` and must follow the [Temporal proposal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal), which uses the format specified in [RFC 9557](https://datatracker.ietf.org/doc/html/rfc9557). +Temporal values are a set of value types, representing either a point in time or the difference between two points in time. They are surrounded by single quotes `'` and must follow the [Temporal proposal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal), which uses a strict version of the format specified in [RFC 9557](https://datatracker.ietf.org/doc/html/rfc9557) (itself based off of ISO 8601 / RFC 3339). -Whitespace between the Temporal value and the single quotes is allowed and ignored. Parsers should validate that the value between single quotes is a valid Temporal value. +Whitespace between the Temporal value and the single quotes is allowed and ignored. Parsers must validate that the value between single quotes is a valid Temporal value. ```duper { + // Allowed + instant: '2022-02-28T03:06:00.092121729Z', + duration: ' P7DT5.000001S ', + calendar_system: '2020-05-22[u-ca=hebrew]', // 28 Iyar 5780 + christmas_eve: '--12-24', + large_extensions: '2020-05-22T07:19:35.123456789-04:00[America/Indiana/Indianapolis][u-ca=islamic-umalqura]', + // Not allowed + not_temporal: 'hello world', // INVALID + "date doesn't exist": '2025-02-29', // INVALID } ``` @@ -292,7 +301,18 @@ These values may or may not contain an [identifier](#identifiers). In the case w ```duper { + // Allowed + precise_identifier: PlainDateTime('2007-03-31T10:35:10'), + subset: PlainYearMonth('1994-11-06T19:45:27-03:00'), // PlainYearMonth is a subset of Instant + // Allowed but discouraged + string_in_disguise: PlainDate("this isn't a Temporal value"), + confusing_identifier: PlainTimeDate('2025-11-03'), /* Unlike `PlainDateTime`, this doesn't + * validate the input, other than that + * it's a Temporal value. */ + + // Not allowed + missing_offset: Instant('2025-11-06T19:39:02.888834'), // INVALID } ```