Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/built-ins.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Implements the [ECMAScript Object](https://developer.mozilla.org/en-US/docs/Web/

### Array (`Goccia.Builtins.GlobalArray.pas`)

Implements the [ECMAScript Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array). **Not implemented:** `Array.prototype.reduceRight`, `Array.prototype.toLocaleString`.
Implements the [ECMAScript Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array). **Not implemented:** `Array.prototype.reduceRight`.

**Static methods:**

Expand Down Expand Up @@ -115,6 +115,7 @@ Implements the [ECMAScript Array](https://developer.mozilla.org/en-US/docs/Web/J
| `includes(value, fromIndex?)` | Check if array contains value |
| `join(separator?)` | Join elements into string |
| `toString()` | Returns comma-separated string (delegates to `join()`) |
| `toLocaleString(locales?, options?)` | Join element locale strings |
| `concat(...arrays)` | Concatenate arrays |
| `slice(start?, end?)` | Extract a section |
| `push(...items)` | Add to end (mutating) |
Expand Down
66 changes: 66 additions & 0 deletions source/units/Goccia.Shims.pas
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,38 @@ implementation
' return Math.trunc(epochMilliseconds);'#10 +
' } catch (e) { return NaN; }'#10 +
'};'#10 +
'const __GocciaShimNumberFormat: any = Intl.NumberFormat;'#10 +
'const __GocciaShimDateTimeFormat: any = Intl.DateTimeFormat;'#10 +
'export const Date = class Date {'#10 +
' static {'#10 +
' const numberLocale = {'#10 +
' toLocaleString(...args: any[]): string {'#10 +
' const value: number = Number.prototype.valueOf.call(this);'#10 +
' return new __GocciaShimNumberFormat(args[0], args[1]).format(value);'#10 +
' }'#10 +
' }.toLocaleString;'#10 +
' const arrayLocale = {'#10 +
' toLocaleString(...args: any[]): string {'#10 +
' const array: any = Object(this);'#10 +
' const length: number = Number(array.length);'#10 +
' const len: number = !Number.isFinite(length) || length <= 0 ? 0 : Math.trunc(length);'#10 +
' const elementString = (index: number): string => {'#10 +
' const nextElement: any = array[index];'#10 +
' if (nextElement !== undefined && nextElement !== null) {'#10 +
' const method: any = Object(nextElement).toLocaleString;'#10 +
' if (typeof method !== "function")'#10 +
' throw new TypeError("Array.prototype.toLocaleString element toLocaleString is not a function");'#10 +
' return String(method.call(nextElement, args[0], args[1]));'#10 +
' }'#10 +
' return "";'#10 +
' };'#10 +
' const build = (index: number, result: string): string => index >= len ? result : build(index + 1, result + (index > 0 ? "," : "") + elementString(index));'#10 +
' return build(0, "");'#10 +
' }'#10 +
' }.toLocaleString;'#10 +
' Object.defineProperty(Number.prototype, "toLocaleString", { value: numberLocale, writable: true, configurable: true });'#10 +
' Object.defineProperty(Array.prototype, "toLocaleString", { value: arrayLocale, writable: true, configurable: true });'#10 +
Comment thread
frostney marked this conversation as resolved.
' }'#10 +
' #ms;'#10 +
' static now(): number { return Temporal.Now.instant().epochMilliseconds; }'#10 +
' static parse(str: string): number {'#10 +
Expand Down Expand Up @@ -244,6 +275,41 @@ 'export const Date = class Date {'#10 +
' return days[z.dayOfWeek % 7] + " " + months[z.month - 1] + " " + pad(z.day) + " " +'#10 +
' String(z.year) + " " + pad(z.hour) + ":" + pad(z.minute) + ":" + pad(z.second) + " GMT" + z.offset;'#10 +
' }'#10 +
' #localeOptions(options: any, needDate: boolean, needTime: boolean): any {'#10 +
' const out: any = {};'#10 +
' if (options !== undefined) {'#10 +
' ["localeMatcher", "calendar", "numberingSystem", "timeZone", "hour12", "hourCycle", "formatMatcher",'#10 +
' "weekday", "era", "year", "month", "day", "dayPeriod", "hour", "minute", "second",'#10 +
' "fractionalSecondDigits", "timeZoneName", "dateStyle", "timeStyle"].forEach((key: string): void => {'#10 +
' const value: any = options[key];'#10 +
' if (value !== undefined) out[key] = value;'#10 +
' });'#10 +
' }'#10 +
' const hasDate = out.weekday !== undefined || out.year !== undefined || out.month !== undefined || out.day !== undefined || out.dateStyle !== undefined;'#10 +
' const hasTime = out.dayPeriod !== undefined || out.hour !== undefined || out.minute !== undefined || out.second !== undefined || out.fractionalSecondDigits !== undefined || out.timeStyle !== undefined;'#10 +
' const needDefaults = !hasDate && !hasTime;'#10 +
' if (needDefaults && needDate) { out.year = "numeric"; out.month = "numeric"; out.day = "numeric"; }'#10 +
' if (needDefaults && needTime) { out.hour = "numeric"; out.minute = "numeric"; out.second = "numeric"; }'#10 +
' return out;'#10 +
' }'#10 +
' toLocaleString(...args: any[]): string {'#10 +
' let valid: boolean;'#10 +
' try { valid = this.#valid(); } catch (e) { throw new TypeError("Date.prototype.toLocaleString called on non-Date"); }'#10 +
' if (!valid) return "Invalid Date";'#10 +
' return new __GocciaShimDateTimeFormat(args[0], this.#localeOptions(args[1], true, true)).format(this.#ms);'#10 +
' }'#10 +
' toLocaleDateString(...args: any[]): string {'#10 +
' let valid: boolean;'#10 +
' try { valid = this.#valid(); } catch (e) { throw new TypeError("Date.prototype.toLocaleDateString called on non-Date"); }'#10 +
' if (!valid) return "Invalid Date";'#10 +
' return new __GocciaShimDateTimeFormat(args[0], this.#localeOptions(args[1], true, false)).format(this.#ms);'#10 +
' }'#10 +
' toLocaleTimeString(...args: any[]): string {'#10 +
' let valid: boolean;'#10 +
' try { valid = this.#valid(); } catch (e) { throw new TypeError("Date.prototype.toLocaleTimeString called on non-Date"); }'#10 +
' if (!valid) return "Invalid Date";'#10 +
' return new __GocciaShimDateTimeFormat(args[0], this.#localeOptions(args[1], false, true)).format(this.#ms);'#10 +
' }'#10 +
'};'
),
( // ES2026 §20.1.3.2 — legacy hasOwnProperty via Object.hasOwn
Expand Down
44 changes: 44 additions & 0 deletions source/units/Goccia.Values.TypedArrayValue.pas
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ TGocciaTypedArrayValue = class(TGocciaInstanceValue)
function TypedArrayReduce(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
function TypedArrayReduceRight(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
function TypedArrayJoin(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
function TypedArrayToLocaleString(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
function TypedArrayToString(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
function TypedArrayToReversed(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
function TypedArrayToSorted(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
Expand Down Expand Up @@ -846,6 +847,7 @@ procedure TGocciaTypedArrayValue.InitializePrototype;
Members.AddMethod(TypedArrayReduce, 2, gmkPrototypeMethod, [gmfNoFunctionPrototype]);
Members.AddMethod(TypedArrayReduceRight, 2, gmkPrototypeMethod, [gmfNoFunctionPrototype]);
Members.AddMethod(TypedArrayJoin, 1, gmkPrototypeMethod, [gmfNoFunctionPrototype]);
Members.AddNamedMethod(PROP_TO_LOCALE_STRING, TypedArrayToLocaleString, 0, gmkPrototypeMethod, [gmfNoFunctionPrototype]);
Members.AddMethod(TypedArrayToString, 0, gmkPrototypeMethod, [gmfNoFunctionPrototype]);
Members.AddMethod(TypedArrayToReversed, 0, gmkPrototypeMethod, [gmfNoFunctionPrototype]);
Members.AddMethod(TypedArrayToSorted, 1, gmkPrototypeMethod, [gmfNoFunctionPrototype]);
Expand Down Expand Up @@ -2129,6 +2131,48 @@ function TGocciaTypedArrayValue.TypedArrayJoin(const AArgs: TGocciaArgumentsColl
Result := TGocciaStringLiteralValue.Create(S);
end;

// ECMA-402 §17.1.6 %TypedArray%.prototype.toLocaleString([locales [, options]])
function TGocciaTypedArrayValue.TypedArrayToLocaleString(const AArgs: TGocciaArgumentsCollection;
const AThisValue: TGocciaValue): TGocciaValue;
var
TA: TGocciaTypedArrayValue;
I, Len: Integer;
S: string;
Element, Method, Formatted: TGocciaValue;
ElementObject: TGocciaObjectValue;
CallArgs: TGocciaArgumentsCollection;
begin
TA := RequireAttachedTypedArray(AThisValue, '%TypedArray%.prototype.toLocaleString');
Len := TA.FLength;
S := '';
for I := 0 to Len - 1 do
begin
if I > 0 then S := S + ',';
Element := TA.GetElementAsValue(I);
ElementObject := ToObject(Element);
Method := ElementObject.GetPropertyWithContext(PROP_TO_LOCALE_STRING, Element);
if (Method = nil) or not Method.IsCallable then
ThrowTypeError('TypedArray.prototype.toLocaleString element toLocaleString is not a function');

CallArgs := TGocciaArgumentsCollection.CreateWithCapacity(2);
try
if AArgs.Length > 0 then
CallArgs.Add(AArgs.GetElement(0))
else
CallArgs.Add(TGocciaUndefinedLiteralValue.UndefinedValue);
if AArgs.Length > 1 then
CallArgs.Add(AArgs.GetElement(1))
else
CallArgs.Add(TGocciaUndefinedLiteralValue.UndefinedValue);
Formatted := InvokeCallable(Method, CallArgs, Element);
finally
CallArgs.Free;
end;
S := S + Formatted.ToStringLiteral.Value;
end;
Result := TGocciaStringLiteralValue.Create(S);
end;

// ES2026 §23.2.3.33 %TypedArray%.prototype.toString()
function TGocciaTypedArrayValue.TypedArrayToString(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
begin
Expand Down
58 changes: 58 additions & 0 deletions tests/built-ins/Array/prototype/toLocaleString.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*---
description: Array.prototype.toLocaleString delegates to each element
features: [Intl, Array.prototype.toLocaleString]
---*/

const isIntl = typeof Intl !== "undefined";

describe("Array.prototype.toLocaleString", () => {
test("exists on Array.prototype", () => {
expect(typeof Array.prototype.toLocaleString).toBe("function");
});

test("invokes each element toLocaleString with locales and options", () => {
const calls = [];
const options = { style: "currency", currency: "EUR" };
const item = {
toLocaleString(locales, receivedOptions) {
calls.push([locales, receivedOptions]);
return "item";
},
};

expect([item, null, undefined, item].toLocaleString("de-DE", options)).toBe("item,,,item");
expect(calls.length).toBe(2);
expect(calls[0][0]).toBe("de-DE");
expect(calls[0][1]).toBe(options);
expect(calls[1][0]).toBe("de-DE");
expect(calls[1][1]).toBe(options);
});

test("throws when an element toLocaleString is not callable", () => {
expect(() => [{ toLocaleString: 1 }].toLocaleString()).toThrow(TypeError);
});

test("does not delegate to mutable Array helpers", () => {
const originalFrom = Array.from;
const originalJoin = Array.prototype.join;

try {
Array.from = () => ["tainted"];
Array.prototype.join = () => "tainted";

expect([1, 2].toLocaleString()).toBe("1,2");
} finally {
Array.from = originalFrom;
Array.prototype.join = originalJoin;
}
});
});

describe.runIf(isIntl)("Array.prototype.toLocaleString Intl elements", () => {
test("formats number elements through Number.prototype.toLocaleString", () => {
const options = { style: "currency", currency: "EUR" };
const expected = new Intl.NumberFormat("de-DE", options).format(1234.5);

expect([1234.5].toLocaleString("de-DE", options)).toBe(expected);
});
});
33 changes: 33 additions & 0 deletions tests/built-ins/Date/prototype/toLocaleDateString.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*---
description: Date.prototype.toLocaleDateString delegates to Intl.DateTimeFormat date defaults
features: [Intl, Date]
---*/

const isIntl = typeof Intl !== "undefined";

describe.runIf(isIntl)("Date.prototype.toLocaleDateString", () => {
test("formats with date defaults", () => {
const date = new Date(Date.UTC(2026, 0, 1, 15, 30, 45));
const options = { timeZone: "UTC" };

expect(date.toLocaleDateString("de-DE", options)).toBe(new Intl.DateTimeFormat("de-DE", {
timeZone: "UTC",
year: "numeric",
month: "numeric",
day: "numeric",
}).format(date));
});

test("preserves explicit date options", () => {
const date = new Date(Date.UTC(2026, 0, 1, 15, 30, 45));
const options = { timeZone: "UTC", year: "numeric", month: "long" };

expect(date.toLocaleDateString("en-US", options)).toBe(new Intl.DateTimeFormat("en-US", options).format(date));
});

test("throws TypeError for fake Date receiver", () => {
const fakeDate = Object.create(Date.prototype);

expect(() => Date.prototype.toLocaleDateString.call(fakeDate)).toThrow(TypeError);
});
});
43 changes: 43 additions & 0 deletions tests/built-ins/Date/prototype/toLocaleString.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*---
description: Date.prototype.toLocaleString delegates to Intl.DateTimeFormat date and time defaults
features: [Intl, Date]
---*/

const isIntl = typeof Intl !== "undefined";

describe.runIf(isIntl)("Date.prototype.toLocaleString", () => {
test("formats with date and time defaults", () => {
const date = new Date(Date.UTC(2026, 0, 1, 15, 30, 45));
const options = { timeZone: "UTC" };

expect(date.toLocaleString("de-DE", options)).toBe(new Intl.DateTimeFormat("de-DE", {
timeZone: "UTC",
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
}).format(date));
});

test("preserves explicit date and time options", () => {
const date = new Date(Date.UTC(2026, 0, 1, 15, 30, 45));
const options = { timeZone: "UTC", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit" };

expect(date.toLocaleString("en-US", options)).toBe(new Intl.DateTimeFormat("en-US", options).format(date));
});

test("preserves style-only options without adding component defaults", () => {
const date = new Date(Date.UTC(2026, 0, 1, 15, 30, 45));
const options = { timeZone: "UTC", dateStyle: "short" };

expect(date.toLocaleString("en-US", options)).toBe(new Intl.DateTimeFormat("en-US", options).format(date));
});

test("throws TypeError for fake Date receiver", () => {
const fakeDate = Object.create(Date.prototype);

expect(() => Date.prototype.toLocaleString.call(fakeDate)).toThrow(TypeError);
});
});
33 changes: 33 additions & 0 deletions tests/built-ins/Date/prototype/toLocaleTimeString.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*---
description: Date.prototype.toLocaleTimeString delegates to Intl.DateTimeFormat time defaults
features: [Intl, Date]
---*/

const isIntl = typeof Intl !== "undefined";

describe.runIf(isIntl)("Date.prototype.toLocaleTimeString", () => {
test("formats with time defaults", () => {
const date = new Date(Date.UTC(2026, 0, 1, 15, 30, 45));
const options = { timeZone: "UTC" };

expect(date.toLocaleTimeString("de-DE", options)).toBe(new Intl.DateTimeFormat("de-DE", {
timeZone: "UTC",
hour: "numeric",
minute: "numeric",
second: "numeric",
}).format(date));
});

test("preserves explicit time options", () => {
const date = new Date(Date.UTC(2026, 0, 1, 15, 30, 45));
const options = { timeZone: "UTC", hour: "2-digit", minute: "2-digit" };

expect(date.toLocaleTimeString("en-US", options)).toBe(new Intl.DateTimeFormat("en-US", options).format(date));
});

test("throws TypeError for fake Date receiver", () => {
const fakeDate = Object.create(Date.prototype);

expect(() => Date.prototype.toLocaleTimeString.call(fakeDate)).toThrow(TypeError);
});
});
28 changes: 28 additions & 0 deletions tests/built-ins/Number/prototype/toLocaleString.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*---
description: Number.prototype.toLocaleString delegates to Intl.NumberFormat
features: [Intl, Number.prototype.toLocaleString]
---*/

const isIntl = typeof Intl !== "undefined";

describe.runIf(isIntl)("Number.prototype.toLocaleString", () => {
test("formats with the requested locale", () => {
const value = 1234567.89;

expect(value.toLocaleString("de-DE")).toBe(new Intl.NumberFormat("de-DE").format(value));
expect(value.toLocaleString("en-US")).toBe(new Intl.NumberFormat("en-US").format(value));
});

test("passes formatting options to Intl.NumberFormat", () => {
const options = { style: "currency", currency: "EUR" };
const value = 1234.5;

expect(value.toLocaleString("de-DE", options)).toBe(new Intl.NumberFormat("de-DE", options).format(value));
});

test("works with Number objects", () => {
const value = new Number(42);

expect(value.toLocaleString("en-US")).toBe(new Intl.NumberFormat("en-US").format(42));
});
});
34 changes: 34 additions & 0 deletions tests/built-ins/TypedArray/prototype/toLocaleString.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*---
description: TypedArray.prototype.toLocaleString delegates to each element
features: [Intl, TypedArray.prototype.toLocaleString]
---*/

const isIntl = typeof Intl !== "undefined";

describe("%TypedArray%.prototype.toLocaleString", () => {
test("invokes number element toLocaleString with locales and options", () => {
const original = Number.prototype.toLocaleString;
const options = { marker: "yes" };

try {
Number.prototype.toLocaleString = {
method(locales, receivedOptions) {
return "number:" + locales + ":" + receivedOptions.marker;
},
}.method;

expect(new Uint8Array([0]).toLocaleString("xx", options)).toBe("number:xx:yes");
} finally {
Number.prototype.toLocaleString = original;
}
});
});

describe.runIf(isIntl)("%TypedArray%.prototype.toLocaleString Intl elements", () => {
test("formats number elements through Number.prototype.toLocaleString", () => {
const options = { minimumFractionDigits: 3 };
const expected = new Intl.NumberFormat("th-u-nu-thai", options).format(0);

expect(new Uint8Array([0]).toLocaleString("th-u-nu-thai", options)).toBe(expected);
});
});
Loading