diff --git a/CHANGELOG.md b/CHANGELOG.md index 90dd48d1ac..32fc2b57c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,35 @@ Detailed changelog for Perry. See CLAUDE.md for concise summaries. +## v0.5.998 — fix(date-fns): format() returns a formatted string instead of undefined + +**Symptom.** `import { format } from 'date-fns'; format(new Date(2020, 0, 6), 'yyyy-MM-dd')` compiled cleanly and ran without error, but the console printed `undefined` instead of `2020-01-06`. Same shape for every other free function date-fns exports (`parseISO`, `addDays`, `isAfter`, …). + +**Root cause — manifest entries existed, dispatch table did not.** Three layers were wired but the middle one was missing: + +1. `crates/perry-api-manifest/src/entries.rs` surfaces `method("date-fns", "format", false, None)` and friends, so the HIR-lowerer recognises the import and emits `NativeMethodCall { module: "date-fns", method: "format", … }`. +2. `crates/perry-stdlib/src/dayjs.rs` defines `js_datefns_format` / `js_datefns_add_days` / etc and `runtime_decls.rs` declares them at the LLVM level. +3. **Missing.** `NATIVE_MODULE_TABLE` in `crates/perry-codegen/src/lower_call.rs` had no `module: "date-fns"` rows, so `native_module_lookup("date-fns", false, "format", _)` returned `None` and the call fell through to the receiver-less-unknown-native branch, which materialises as undefined. + +Beyond the dispatch hole, `js_datefns_format` itself was a thin wrapper around `js_dayjs_format`, which only knows dayjs's *uppercase* tokens (`YYYY/DD`). date-fns uses *lowercase* Unicode-LDML tokens (`yyyy/dd`) — so even after the dispatch was hooked up, `format(d, 'yyyy-MM-dd')` would have printed the raw template back. And dayjs's formatter normalises to UTC, which shifts the displayed day across the midnight boundary for any non-UTC machine. + +**Fix.** Two halves: + +1. **Wire dispatch** (`crates/perry-codegen/src/lower_call.rs`). Added twelve `NativeModSig` rows for module `"date-fns"` covering the manifest surface (`format`, `parseISO`, `addDays/Months/Years`, `differenceInDays/Hours/Minutes`, `isAfter/Before`, `startOfDay/endOfDay`). All `has_receiver: false` — date-fns is a functional API, every call passes the date as the first argument. +2. **Rewrite `js_datefns_format`** (`crates/perry-stdlib/src/dayjs.rs`). New `format_date_fns_pattern` helper walks the pattern character-by-character, recognises the LDML tokens date-fns actually emits (`y/M/d/H/h/m/s/S/a/E/X`), handles `'single-quoted literals'`, and formats in `chrono::Local` so the day-of-month matches Node's date-fns output on a non-UTC machine. Token-run length controls padding (e.g. `M` → `1`, `MM` → `01`, `MMM` → `Jan`, `MMMM` → `January`). + +**Validation.** New test fixture `test-files/test_date_fns_format.ts` exercises the regex-token path (existing) plus end-to-end `format(...)`. Byte-for-byte parity against Node's `--experimental-strip-types` on the original repro: + +``` +import { format } from 'date-fns'; +const d = new Date(2020, 0, 6); +console.log(format(d, 'yyyy-MM-dd')); // 2020-01-06 +console.log(format(d, 'yyyy')); // 2020 +console.log(format(d, 'MM')); // 01 +console.log(format(d, 'dd')); // 06 +console.log(typeof format(d, 'yyyy')); // string +``` + ## v0.5.997 — feat(jsruntime): V8 named-import static-method dispatch for Effect.succeed **Symptom.** `import { Effect } from 'effect'; Effect.succeed(42)` returned the literal number `0` instead of an Effect class instance. `typeof e === 'number'`, `e.pipe` was `undefined`, `e._tag` was `undefined`. Same shape blocked any `import { X } from 'v8-fallback-pkg'; X.method(args)` pattern where the V8 module's top-level export `X` is itself a sub-namespace object holding the actual functions — the Effect/jose/many-internal-tools pattern. diff --git a/CLAUDE.md b/CLAUDE.md index 5a90de9706..24034a0ba2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.997 +**Current Version:** 0.5.998 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index e01bf2a6a9..a09635dc5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4905,7 +4905,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "base64", @@ -4960,14 +4960,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.997" +version = "0.5.998" dependencies = [ "serde", ] [[package]] name = "perry-codegen" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "log", @@ -4980,7 +4980,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "perry-hir", @@ -4989,7 +4989,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "perry-hir", @@ -4997,7 +4997,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "perry-dispatch", @@ -5007,7 +5007,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "perry-hir", @@ -5016,7 +5016,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "base64", @@ -5029,7 +5029,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "perry-hir", @@ -5037,7 +5037,7 @@ dependencies = [ [[package]] name = "perry-diagnostics" -version = "0.5.997" +version = "0.5.998" dependencies = [ "serde", "serde_json", @@ -5045,7 +5045,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.997" +version = "0.5.998" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5056,7 +5056,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "clap", @@ -5071,7 +5071,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.997" +version = "0.5.998" dependencies = [ "argon2", "perry-ffi", @@ -5079,7 +5079,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", "reqwest", @@ -5088,7 +5088,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.997" +version = "0.5.998" dependencies = [ "bcrypt", "perry-ffi", @@ -5096,7 +5096,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", "rusqlite", @@ -5104,7 +5104,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", "scraper", @@ -5112,14 +5112,14 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-cron" -version = "0.5.997" +version = "0.5.998" dependencies = [ "chrono", "cron", @@ -5128,7 +5128,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.997" +version = "0.5.998" dependencies = [ "chrono", "perry-ffi", @@ -5136,7 +5136,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", "rust_decimal", @@ -5144,7 +5144,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", "serde_json", @@ -5152,7 +5152,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", "rand 0.8.6", @@ -5160,21 +5160,21 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.997" +version = "0.5.998" dependencies = [ "bytes", "http-body-util", @@ -5188,7 +5188,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.997" +version = "0.5.998" dependencies = [ "lazy_static", "perry-ffi", @@ -5199,7 +5199,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.997" +version = "0.5.998" dependencies = [ "lazy_static", "perry-ext-http-server", @@ -5211,7 +5211,7 @@ dependencies = [ [[package]] name = "perry-ext-http-server" -version = "0.5.997" +version = "0.5.998" dependencies = [ "bytes", "http-body-util", @@ -5230,7 +5230,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.997" +version = "0.5.998" dependencies = [ "lazy_static", "perry-ffi", @@ -5240,7 +5240,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.997" +version = "0.5.998" dependencies = [ "base64", "jsonwebtoken", @@ -5251,7 +5251,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.997" +version = "0.5.998" dependencies = [ "lru", "perry-ffi", @@ -5259,7 +5259,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.997" +version = "0.5.998" dependencies = [ "chrono", "perry-ffi", @@ -5267,7 +5267,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.997" +version = "0.5.998" dependencies = [ "bson", "futures-util", @@ -5279,7 +5279,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.997" +version = "0.5.998" dependencies = [ "chrono", "perry-ffi", @@ -5289,7 +5289,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.997" +version = "0.5.998" dependencies = [ "nanoid", "perry-ffi", @@ -5298,7 +5298,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", "rustls", @@ -5309,7 +5309,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.997" +version = "0.5.998" dependencies = [ "lettre", "perry-ffi", @@ -5319,7 +5319,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", "sqlx", @@ -5328,7 +5328,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.997" +version = "0.5.998" dependencies = [ "governor", "perry-ffi", @@ -5336,7 +5336,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.997" +version = "0.5.998" dependencies = [ "base64", "image", @@ -5345,14 +5345,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.997" +version = "0.5.998" dependencies = [ "lazy_static", "perry-ffi", @@ -5360,7 +5360,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", "uuid", @@ -5368,7 +5368,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.997" +version = "0.5.998" dependencies = [ "perry-ffi", "regex", @@ -5378,7 +5378,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.997" +version = "0.5.998" dependencies = [ "futures-util", "lazy_static", @@ -5389,7 +5389,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.997" +version = "0.5.998" dependencies = [ "flate2", "perry-ffi", @@ -5397,7 +5397,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.997" +version = "0.5.998" dependencies = [ "dashmap 6.1.0", "once_cell", @@ -5406,7 +5406,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "perry-api-manifest", @@ -5420,7 +5420,7 @@ dependencies = [ [[package]] name = "perry-jsruntime" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "deno_core", @@ -5440,7 +5440,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "perry-diagnostics", @@ -5452,7 +5452,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "base64", @@ -5476,7 +5476,7 @@ dependencies = [ [[package]] name = "perry-stdlib" -version = "0.5.997" +version = "0.5.998" dependencies = [ "aes 0.8.4", "aes-gcm", @@ -5546,7 +5546,7 @@ dependencies = [ [[package]] name = "perry-transform" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "perry-hir", @@ -5556,7 +5556,7 @@ dependencies = [ [[package]] name = "perry-types" -version = "0.5.997" +version = "0.5.998" dependencies = [ "anyhow", "thiserror 1.0.69", @@ -5564,11 +5564,11 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.997" +version = "0.5.998" [[package]] name = "perry-ui-android" -version = "0.5.997" +version = "0.5.998" dependencies = [ "itoa", "jni", @@ -5583,7 +5583,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.997" +version = "0.5.998" dependencies = [ "rand 0.8.6", "serde", @@ -5593,7 +5593,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.997" +version = "0.5.998" dependencies = [ "cairo-rs", "dirs 5.0.1", @@ -5612,7 +5612,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.997" +version = "0.5.998" dependencies = [ "block2", "libc", @@ -5627,7 +5627,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.997" +version = "0.5.998" dependencies = [ "block2", "libc", @@ -5645,11 +5645,11 @@ version = "0.1.0" [[package]] name = "perry-ui-testkit" -version = "0.5.997" +version = "0.5.998" [[package]] name = "perry-ui-tvos" -version = "0.5.997" +version = "0.5.998" dependencies = [ "block2", "libc", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.997" +version = "0.5.998" dependencies = [ "block2", "libc", @@ -5679,7 +5679,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.997" +version = "0.5.998" dependencies = [ "block2", "libc", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.997" +version = "0.5.998" dependencies = [ "libc", "perry-runtime", @@ -5706,7 +5706,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.997" +version = "0.5.998" dependencies = [ "base64", "ed25519-dalek", @@ -5720,7 +5720,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.997" +version = "0.5.998" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 263afa12d3..5bc9d23b70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -190,7 +190,7 @@ opt-level = "s" # Optimize for size in stdlib opt-level = 3 [workspace.package] -version = "0.5.997" +version = "0.5.998" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/crates/perry-codegen/src/lower_call.rs b/crates/perry-codegen/src/lower_call.rs index 35a7189303..ded2378c45 100644 --- a/crates/perry-codegen/src/lower_call.rs +++ b/crates/perry-codegen/src/lower_call.rs @@ -8975,6 +8975,121 @@ const NATIVE_MODULE_TABLE: &[NativeModSig] = &[ args: &[], ret: NR_F64, }, + // ========== date-fns ========== + // date-fns exports free functions: `format(date, pattern)`, + // `addDays(date, n)`, etc. The first argument is a Date (NaN-boxed + // f64 timestamp from `new Date(...)`). The manifest entries surface + // these as receiver-less NativeMethodCalls on module "date-fns". + // Without these rows the dispatch returns None and the call falls + // through to undefined. Refs date-fns format() blocker. + NativeModSig { + module: "date-fns", + has_receiver: false, + method: "format", + class_filter: None, + runtime: "js_datefns_format", + args: &[NA_F64, NA_STR], + ret: NR_STR, + }, + NativeModSig { + module: "date-fns", + has_receiver: false, + method: "parseISO", + class_filter: None, + runtime: "js_datefns_parse_iso", + args: &[NA_STR], + ret: NR_F64, + }, + NativeModSig { + module: "date-fns", + has_receiver: false, + method: "addDays", + class_filter: None, + runtime: "js_datefns_add_days", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "date-fns", + has_receiver: false, + method: "addMonths", + class_filter: None, + runtime: "js_datefns_add_months", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "date-fns", + has_receiver: false, + method: "addYears", + class_filter: None, + runtime: "js_datefns_add_years", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "date-fns", + has_receiver: false, + method: "differenceInDays", + class_filter: None, + runtime: "js_datefns_difference_in_days", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "date-fns", + has_receiver: false, + method: "differenceInHours", + class_filter: None, + runtime: "js_datefns_difference_in_hours", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "date-fns", + has_receiver: false, + method: "differenceInMinutes", + class_filter: None, + runtime: "js_datefns_difference_in_minutes", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "date-fns", + has_receiver: false, + method: "isAfter", + class_filter: None, + runtime: "js_datefns_is_after", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "date-fns", + has_receiver: false, + method: "isBefore", + class_filter: None, + runtime: "js_datefns_is_before", + args: &[NA_F64, NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "date-fns", + has_receiver: false, + method: "startOfDay", + class_filter: None, + runtime: "js_datefns_start_of_day", + args: &[NA_F64], + ret: NR_F64, + }, + NativeModSig { + module: "date-fns", + has_receiver: false, + method: "endOfDay", + class_filter: None, + runtime: "js_datefns_end_of_day", + args: &[NA_F64], + ret: NR_F64, + }, // ========== moment ========== // Only factory wired: moment instance methods take f64 handle (not i64), // incompatible with the has_receiver:true i64-first-arg dispatch ABI. diff --git a/crates/perry-stdlib/src/dayjs.rs b/crates/perry-stdlib/src/dayjs.rs index 83ade739b3..c2984fdcfb 100644 --- a/crates/perry-stdlib/src/dayjs.rs +++ b/crates/perry-stdlib/src/dayjs.rs @@ -520,16 +520,219 @@ pub unsafe extern "C" fn js_dayjs_is_valid(handle: Handle) -> f64 { // ============ date-fns compatible functions ============ /// format(date, formatStr) -> string (date-fns compatible) +/// +/// date-fns uses Unicode-LDML-ish tokens (lowercase `yyyy` for year, +/// `dd` for day-of-month, `MM` for month, etc.) and formats in the +/// **local** timezone — distinct from dayjs which uses uppercase +/// `YYYY`/`DD` and is happy to format in UTC. We can't reuse +/// `js_dayjs_format` for this because (a) the token replacements miss, +/// and (b) local-vs-UTC swings the day across a midnight boundary. +/// Refs date-fns blocker. #[no_mangle] pub unsafe extern "C" fn js_datefns_format( timestamp: f64, pattern_ptr: *const StringHeader, ) -> *mut StringHeader { - let handle_f64 = js_dayjs_from_timestamp(timestamp); - if handle_f64 == 0.0 { + use chrono::Local; + if timestamp.is_nan() { return std::ptr::null_mut(); } - js_dayjs_format(f64_to_handle(handle_f64), pattern_ptr) + let pattern = string_from_header(pattern_ptr).unwrap_or_else(|| "yyyy-MM-dd".to_string()); + // `new Date(year, monthIndex, ...)` stores a UTC ms timestamp whose + // wall-clock representation in local time is the literal components + // the user passed. So formatting it in `Local` reproduces those + // components — which is exactly what Node's date-fns does. + let secs = (timestamp / 1000.0) as i64; + let nanos = ((timestamp.rem_euclid(1000.0)) * 1_000_000.0) as u32; + let dt = match chrono::DateTime::from_timestamp(secs, nanos) { + Some(d) => d.with_timezone(&Local), + None => return std::ptr::null_mut(), + }; + let formatted = format_date_fns_pattern(&pattern, &dt); + js_string_from_bytes(formatted.as_ptr(), formatted.len() as u32) +} + +/// Translate a date-fns format pattern against a chrono datetime. +/// +/// Handles the most common Unicode-LDML tokens that date-fns supports. +/// Single-quoted literals (`'literal'`) pass through untouched. Tokens +/// not recognized below pass through unchanged — matching date-fns' +/// permissive behavior on unknown letters. +fn format_date_fns_pattern(pattern: &str, dt: &chrono::DateTime) -> String +where + Tz::Offset: std::fmt::Display, +{ + use chrono::{Datelike, Timelike}; + let bytes = pattern.as_bytes(); + let mut out = String::with_capacity(pattern.len() + 8); + let mut i = 0; + while i < bytes.len() { + let c = bytes[i]; + // Single-quoted literal — anything between `'...'` is emitted + // verbatim. `''` is a single quote. + if c == b'\'' { + i += 1; + if i < bytes.len() && bytes[i] == b'\'' { + out.push('\''); + i += 1; + continue; + } + while i < bytes.len() && bytes[i] != b'\'' { + out.push(bytes[i] as char); + i += 1; + } + if i < bytes.len() { + i += 1; + } + continue; + } + if !c.is_ascii_alphabetic() { + out.push(c as char); + i += 1; + continue; + } + // Read a run of the same letter. + let start = i; + while i < bytes.len() && bytes[i] == c { + i += 1; + } + let run = i - start; + match c { + b'y' => match run { + 1 => out.push_str(&format!("{}", dt.year())), + 2 => out.push_str(&format!("{:02}", dt.year() % 100)), + _ => out.push_str(&format!("{:0width$}", dt.year(), width = run)), + }, + b'M' => match run { + 1 => out.push_str(&format!("{}", dt.month())), + 2 => out.push_str(&format!("{:02}", dt.month())), + 3 => out.push_str(short_month_name(dt.month())), + _ => out.push_str(long_month_name(dt.month())), + }, + b'd' => match run { + 1 => out.push_str(&format!("{}", dt.day())), + _ => out.push_str(&format!("{:02}", dt.day())), + }, + b'H' => match run { + 1 => out.push_str(&format!("{}", dt.hour())), + _ => out.push_str(&format!("{:02}", dt.hour())), + }, + b'h' => { + let h12 = dt.hour() % 12; + let h12 = if h12 == 0 { 12 } else { h12 }; + match run { + 1 => out.push_str(&format!("{}", h12)), + _ => out.push_str(&format!("{:02}", h12)), + } + } + b'm' => match run { + 1 => out.push_str(&format!("{}", dt.minute())), + _ => out.push_str(&format!("{:02}", dt.minute())), + }, + b's' => match run { + 1 => out.push_str(&format!("{}", dt.second())), + _ => out.push_str(&format!("{:02}", dt.second())), + }, + b'S' => { + // Fractional seconds. date-fns uses S/SS/SSS for 1/2/3 + // digits of millisecond precision. + let ms = dt.nanosecond() / 1_000_000; + match run { + 1 => out.push_str(&format!("{}", ms / 100)), + 2 => out.push_str(&format!("{:02}", ms / 10)), + _ => out.push_str(&format!("{:03}", ms)), + } + } + b'a' => { + // am/pm marker + let ampm = if dt.hour() < 12 { "AM" } else { "PM" }; + out.push_str(ampm); + } + b'E' => { + // Day-of-week abbreviations. EEEE = long, EEE/EE/E = short. + let wd = dt.weekday(); + if run >= 4 { + out.push_str(long_weekday_name(wd)); + } else { + out.push_str(short_weekday_name(wd)); + } + } + b'X' | b'x' => { + // Time-zone offset. Approximate as ±HH:MM (date-fns has + // many variants; this covers the common case). + out.push_str(&format!("{}", dt.offset())); + } + _ => { + // Unknown letter run — emit verbatim (date-fns throws + // on truly unknown tokens, but conservatively passing + // through is closer to dayjs's behavior). + for _ in 0..run { + out.push(c as char); + } + } + } + } + out +} + +fn short_month_name(m: u32) -> &'static str { + match m { + 1 => "Jan", + 2 => "Feb", + 3 => "Mar", + 4 => "Apr", + 5 => "May", + 6 => "Jun", + 7 => "Jul", + 8 => "Aug", + 9 => "Sep", + 10 => "Oct", + 11 => "Nov", + 12 => "Dec", + _ => "", + } +} + +fn long_month_name(m: u32) -> &'static str { + match m { + 1 => "January", + 2 => "February", + 3 => "March", + 4 => "April", + 5 => "May", + 6 => "June", + 7 => "July", + 8 => "August", + 9 => "September", + 10 => "October", + 11 => "November", + 12 => "December", + _ => "", + } +} + +fn short_weekday_name(w: chrono::Weekday) -> &'static str { + match w { + chrono::Weekday::Mon => "Mon", + chrono::Weekday::Tue => "Tue", + chrono::Weekday::Wed => "Wed", + chrono::Weekday::Thu => "Thu", + chrono::Weekday::Fri => "Fri", + chrono::Weekday::Sat => "Sat", + chrono::Weekday::Sun => "Sun", + } +} + +fn long_weekday_name(w: chrono::Weekday) -> &'static str { + match w { + chrono::Weekday::Mon => "Monday", + chrono::Weekday::Tue => "Tuesday", + chrono::Weekday::Wed => "Wednesday", + chrono::Weekday::Thu => "Thursday", + chrono::Weekday::Fri => "Friday", + chrono::Weekday::Sat => "Saturday", + chrono::Weekday::Sun => "Sunday", + } } /// parseISO(dateString) -> timestamp (date-fns compatible) diff --git a/test-files/test_date_fns_format.ts b/test-files/test_date_fns_format.ts index 6406d3e8ae..4b9abc4147 100644 --- a/test-files/test_date_fns_format.ts +++ b/test-files/test_date_fns_format.ts @@ -27,3 +27,13 @@ console.log("aabbbccdd".match(g)); // No-match returns null console.log("xyz".match(/(\w)\1+/)); + +// End-to-end: format(date, pattern). Uses the native runtime path +// (date-fns is well-known-aliased to perry-ext-dayjs). +import { format } from "date-fns"; +const d = new Date(2020, 0, 6); +console.log(format(d, "yyyy-MM-dd")); +console.log(format(d, "yyyy")); +console.log(format(d, "MM")); +console.log(format(d, "dd")); +console.log(typeof format(d, "yyyy"));