Skip to content

Commit

Permalink
Merge branch 'main' into feat/3857-infer-columns
Browse files Browse the repository at this point in the history
  • Loading branch information
kt3k authored Nov 13, 2024
2 parents 0c8bb4f + 9607eb8 commit 953f705
Show file tree
Hide file tree
Showing 18 changed files with 698 additions and 91 deletions.
2 changes: 2 additions & 0 deletions _tools/check_docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const ENTRY_POINTS = [
"../json/mod.ts",
"../jsonc/mod.ts",
"../log/base_handler.ts",
"../log/file_handler.ts",
"../log/warn.ts",
"../log/critical.ts",
"../log/debug.ts",
Expand All @@ -81,6 +82,7 @@ const ENTRY_POINTS = [
"../log/console_handler.ts",
"../log/formatters.ts",
"../log/get_logger.ts",
"../log/logger.ts",
"../media_types/mod.ts",
"../msgpack/mod.ts",
"../net/mod.ts",
Expand Down
174 changes: 115 additions & 59 deletions assert/equal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,71 +6,126 @@ function isKeyedCollection(x: unknown): x is KeyedCollection {
return x instanceof Set || x instanceof Map;
}

function constructorsEqual(a: object, b: object) {
return a.constructor === b.constructor ||
a.constructor === Object && !b.constructor ||
!a.constructor && b.constructor === Object;
function prototypesEqual(a: object, b: object) {
const pa = Object.getPrototypeOf(a);
const pb = Object.getPrototypeOf(b);
return pa === pb ||
pa === Object.prototype && pb === null ||
pa === null && pb === Object.prototype;
}

function isBasicObjectOrArray(obj: object) {
const proto = Object.getPrototypeOf(obj);
return proto === null || proto === Object.prototype ||
proto === Array.prototype;
}

// Slightly faster than Reflect.ownKeys in V8 as of 12.9.202.13-rusty (2024-10-28)
function ownKeys(obj: object) {
return [
...Object.getOwnPropertyNames(obj),
...Object.getOwnPropertySymbols(obj),
];
}

function getKeysDeep(obj: object) {
const keys = new Set<string | symbol>();

while (obj !== Object.prototype && obj !== Array.prototype && obj != null) {
for (const key of ownKeys(obj)) {
keys.add(key);
}
obj = Object.getPrototypeOf(obj);
}

return keys;
}

// deno-lint-ignore no-explicit-any
const Temporal: any = (globalThis as any).Temporal ??
new Proxy({}, { get: () => {} });

/** A non-exhaustive list of prototypes that can be accurately fast-path compared with `String(instance)` */
const stringComparablePrototypes = new Set<unknown>(
[
Intl.Locale,
RegExp,
Temporal.Duration,
Temporal.Instant,
Temporal.PlainDate,
Temporal.PlainDateTime,
Temporal.PlainTime,
Temporal.PlainYearMonth,
Temporal.PlainMonthDay,
Temporal.ZonedDateTime,
URL,
URLSearchParams,
].filter((x) => x != null).map((x) => x.prototype),
);

function isPrimitive(x: unknown) {
return typeof x === "string" ||
typeof x === "number" ||
typeof x === "boolean" ||
typeof x === "bigint" ||
typeof x === "symbol" ||
x == null;
}

type TypedArray = Pick<Uint8Array | BigUint64Array, "length" | number>;
const TypedArray = Object.getPrototypeOf(Uint8Array);
function compareTypedArrays(a: TypedArray, b: TypedArray) {
if (a.length !== b.length) return false;
for (let i = 0; i < b.length; i++) {
if (!sameValueZero(a[i], b[i])) return false;
}
return true;
}

/** Check both strict equality (`0 == -0`) and `Object.is` (`NaN == NaN`) */
function sameValueZero(a: unknown, b: unknown) {
return a === b || Object.is(a, b);
}

/**
* Deep equality comparison used in assertions.
*
* @param c The actual value
* @param d The expected value
* @param a The actual value
* @param b The expected value
* @returns `true` if the values are deeply equal, `false` otherwise
*
* @example Usage
* ```ts
* import { equal } from "@std/assert/equal";
*
* equal({ foo: "bar" }, { foo: "bar" }); // Returns `true`
* equal({ foo: "bar" }, { foo: "baz" }); // Returns `false
* equal({ foo: "bar" }, { foo: "baz" }); // Returns `false`
* ```
*/
export function equal(c: unknown, d: unknown): boolean {
const seen = new Map();
export function equal(a: unknown, b: unknown): boolean {
const seen = new Map<unknown, unknown>();
return (function compare(a: unknown, b: unknown): boolean {
// Have to render RegExp & Date for string comparison
// unless it's mistreated as object
if (
a &&
b &&
((a instanceof RegExp && b instanceof RegExp) ||
(a instanceof URL && b instanceof URL))
) {
return String(a) === String(b);
}
if (sameValueZero(a, b)) return true;
if (isPrimitive(a) || isPrimitive(b)) return false;

if (a instanceof Date && b instanceof Date) {
const aTime = a.getTime();
const bTime = b.getTime();
// Check for NaN equality manually since NaN is not
// equal to itself.
if (Number.isNaN(aTime) && Number.isNaN(bTime)) {
return true;
}
return aTime === bTime;
}
if (typeof a === "number" && typeof b === "number") {
return Number.isNaN(a) && Number.isNaN(b) || a === b;
}
if (Object.is(a, b)) {
return true;
return Object.is(a.getTime(), b.getTime());
}
if (a && typeof a === "object" && b && typeof b === "object") {
if (a && b && !constructorsEqual(a, b)) {
if (!prototypesEqual(a, b)) {
return false;
}
if (a instanceof WeakMap || b instanceof WeakMap) {
if (!(a instanceof WeakMap && b instanceof WeakMap)) return false;
if (a instanceof TypedArray) {
return compareTypedArrays(a as TypedArray, b as TypedArray);
}
if (a instanceof WeakMap) {
throw new TypeError("cannot compare WeakMap instances");
}
if (a instanceof WeakSet || b instanceof WeakSet) {
if (!(a instanceof WeakSet && b instanceof WeakSet)) return false;
if (a instanceof WeakSet) {
throw new TypeError("cannot compare WeakSet instances");
}
if (a instanceof WeakRef || b instanceof WeakRef) {
if (!(a instanceof WeakRef && b instanceof WeakRef)) return false;
return compare(a.deref(), b.deref());
if (a instanceof WeakRef) {
return compare(a.deref(), (b as WeakRef<WeakKey>).deref());
}
if (seen.get(a) === b) {
return true;
Expand All @@ -85,14 +140,7 @@ export function equal(c: unknown, d: unknown): boolean {
}

const aKeys = [...a.keys()];
const primitiveKeysFastPath = aKeys.every((k) => {
return typeof k === "string" ||
typeof k === "number" ||
typeof k === "boolean" ||
typeof k === "bigint" ||
typeof k === "symbol" ||
k == null;
});
const primitiveKeysFastPath = aKeys.every(isPrimitive);
if (primitiveKeysFastPath) {
if (a instanceof Set) {
return a.symmetricDifference(b).size === 0;
Expand Down Expand Up @@ -130,15 +178,23 @@ export function equal(c: unknown, d: unknown): boolean {

return unmatchedEntries === 0;
}
const merged = { ...a, ...b };
for (
const key of [
...Object.getOwnPropertyNames(merged),
...Object.getOwnPropertySymbols(merged),
]
) {
type Key = keyof typeof merged;
if (!compare(a && a[key as Key], b && b[key as Key])) {

let keys: Iterable<string | symbol>;

if (isBasicObjectOrArray(a)) {
// fast path
keys = ownKeys({ ...a, ...b });
} else if (stringComparablePrototypes.has(Object.getPrototypeOf(a))) {
// medium path
return String(a) === String(b);
} else {
// slow path
keys = getKeysDeep(a).union(getKeysDeep(b));
}

for (const key of keys) {
type Key = keyof typeof a;
if (!compare(a[key as Key], b[key as Key])) {
return false;
}
if (((key in a) && (!(key in b))) || ((key in b) && (!(key in a)))) {
Expand All @@ -148,5 +204,5 @@ export function equal(c: unknown, d: unknown): boolean {
return true;
}
return false;
})(c, d);
})(a, b);
}
146 changes: 146 additions & 0 deletions assert/equal_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,149 @@ Deno.test("equal() keyed collection edge cases", () => {
new Map([[1, { a: 1 }], [2, { b: 3 }]]),
));
});

Deno.test("equal() with constructor and prototype", async (t) => {
await t.step(
"value-equal but reference-unequal property named `constructor`",
() => {
assert(equal(
{ constructor: { x: 1 } },
{ constructor: { x: 1 } },
));
},
);
await t.step(
"instance vs plain object with property named `constructor`",
() => {
class X {}
const a = new X();
const b = { constructor: X };
assert(equal(a.constructor, b.constructor));
assertFalse(equal(a, b));
},
);
await t.step("manually set prototype", () => {
class X {
prop = 1;
}
const a = new X();
const b = {} as X;

// false as prototype differs
assertFalse(equal(a, b));
Object.setPrototypeOf(b, X.prototype);
// still false as `prop` is not set
assertFalse(equal(a, b));
b.prop = 1;
// now true
assert(equal(a, b));
});
});

Deno.test("equal() with dynamic properties defined on the prototype", async (t) => {
await t.step("built-in web APIs", async (t) => {
await t.step("URLPattern", () => {
assert(equal(
new URLPattern("*://*.*"),
new URLPattern("*://*.*"),
));

assertFalse(equal(
new URLPattern("*://*.*"),
new URLPattern("*://*.com"),
));
});

await t.step("URLSearchParams", () => {
assert(equal(
new URLSearchParams("a=1&b=2"),
new URLSearchParams("a=1&b=2"),
));

assertFalse(equal(
new URLSearchParams("a=1&b=2"),
new URLSearchParams("a=1&b=99999"),
));
});

await t.step("Intl.Locale", () => {
assert(equal(
new Intl.Locale("es-MX"),
new Intl.Locale("es-MX"),
));

assertFalse(equal(
new Intl.Locale("es-MX"),
new Intl.Locale("pt-BR"),
));
});
});

await t.step("custom prototype", () => {
type Obj = {
prop: number;
};
const wm = new WeakMap<Obj, number>();
const proto: Obj = {
get prop() {
return wm.get(this)!;
},
};
const makeObj = (val: number): Obj => {
const x = Object.create(proto);
wm.set(x, val);
return x;
};

assert(equal(
makeObj(1),
makeObj(1),
));

assertFalse(equal(
makeObj(1),
makeObj(99999),
));
});
});

Deno.test("equal() with typed arrays", async (t) => {
await t.step("Uint8Array", async (t) => {
await t.step("equal", () => {
assert(equal(
new Uint8Array([1, 2, 3]),
new Uint8Array([1, 2, 3]),
));
});

await t.step("unequal", () => {
assertFalse(equal(
new Uint8Array([1, 2, 3]),
new Uint8Array([1, 2, 4]),
));
});

await t.step("length unequal", () => {
assertFalse(equal(
new Uint8Array([0]),
new Uint8Array([0, 0]),
));
});
});

await t.step("Float64Array", async (t) => {
await t.step("NaN == NaN", () => {
assert(equal(
new Float64Array([NaN]),
new Float64Array([NaN]),
));
});

await t.step("0 == -0", () => {
assert(equal(
new Float64Array([0]),
new Float64Array([-0]),
));
});
});
});
Loading

0 comments on commit 953f705

Please sign in to comment.