Skip to content

Commit

Permalink
Value deep equality function (#358)
Browse files Browse the repository at this point in the history
  • Loading branch information
macjuul authored Oct 11, 2024
1 parent ca33f4a commit eeddc8c
Show file tree
Hide file tree
Showing 14 changed files with 246 additions and 86 deletions.
10 changes: 9 additions & 1 deletion src/data/types/decimal.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
export class Decimal {
import { Value } from "../value";

export class Decimal extends Value {
readonly decimal: string;

constructor(decimal: string | number | Decimal) {
super();
this.decimal = decimal.toString();
}

equals(other: unknown): boolean {
if (!(other instanceof Decimal)) return false;
return this.decimal === other.decimal;
}

toString(): string {
return this.decimal;
}
Expand Down
10 changes: 9 additions & 1 deletion src/data/types/duration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SurrealDbError } from "../../errors";
import { Value } from "../value";

const millisecond = 1;
const microsecond = millisecond / 1000;
Expand Down Expand Up @@ -31,10 +32,12 @@ const durationPartRegex = new RegExp(
`^(\\d+)(${Array.from(units.keys()).join("|")})`,
);

export class Duration {
export class Duration extends Value {
readonly _milliseconds: number;

constructor(input: Duration | number | string) {
super();

if (input instanceof Duration) {
this._milliseconds = input._milliseconds;
} else if (typeof input === "string") {
Expand All @@ -51,6 +54,11 @@ export class Duration {
return new Duration(ms);
}

equals(other: unknown): boolean {
if (!(other instanceof Duration)) return false;
return this._milliseconds === other._milliseconds;
}

toCompact(): [number, number] | [number] | [] {
const s = Math.floor(this._milliseconds / 1000);
const ns = Math.floor((this._milliseconds - s * 1000) * 1000000);
Expand Down
13 changes: 11 additions & 2 deletions src/data/types/future.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
export class Future {
constructor(readonly inner: string) {}
import { Value } from "../value";

export class Future extends Value {
constructor(readonly inner: string) {
super();
}

equals(other: unknown): boolean {
if (!(other instanceof Future)) return false;
return this.inner === other.inner;
}

toJSON(): string {
return this.toString();
Expand Down
12 changes: 11 additions & 1 deletion src/data/types/geometry.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { Value } from "../value.ts";
import { Decimal } from "./decimal.ts";

export abstract class Geometry {
export abstract class Geometry extends Value {
abstract toJSON(): GeoJson;
abstract is(geometry: Geometry): boolean;
abstract clone(): Geometry;

equals(other: unknown): boolean {
if (!(other instanceof Geometry)) return false;
return this.is(other);
}

toString(): string {
return JSON.stringify(this.toJSON());
}
}

function f(num: number | Decimal) {
Expand Down
32 changes: 29 additions & 3 deletions src/data/types/range.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import { Tagged } from "../../cbor";
import { SurrealDbError } from "../../errors";
import { equals } from "../../util/equals";
import { toSurrealqlString } from "../../util/to-surrealql-string";
import { TAG_BOUND_EXCLUDED, TAG_BOUND_INCLUDED } from "../cbor";
import { Value } from "../value";
import {
type RecordIdValue,
escape_id_part,
escape_ident,
isValidIdPart,
} from "./recordid";

export class Range<Beg, End> {
export class Range<Beg, End> extends Value {
constructor(
readonly beg: Bound<Beg>,
readonly end: Bound<End>,
) {}
) {
super();
}

equals(other: unknown): boolean {
if (!(other instanceof Range)) return false;
if (this.beg?.constructor !== other.beg?.constructor) return false;
if (this.end?.constructor !== other.end?.constructor) return false;
return (
equals(this.beg?.value, other.beg?.value) &&
equals(this.end?.value, other.end?.value)
);
}

toJSON(): string {
return this.toString();
Expand All @@ -35,18 +49,30 @@ export class BoundExcluded<T> {
constructor(readonly value: T) {}
}

export class RecordIdRange<Tb extends string = string> {
export class RecordIdRange<Tb extends string = string> extends Value {
constructor(
public readonly tb: Tb,
public readonly beg: Bound<RecordIdValue>,
public readonly end: Bound<RecordIdValue>,
) {
super();
if (typeof tb !== "string")
throw new SurrealDbError("TB part is not valid");
if (!isValidIdBound(beg)) throw new SurrealDbError("Beg part is not valid");
if (!isValidIdBound(end)) throw new SurrealDbError("End part is not valid");
}

equals(other: unknown): boolean {
if (!(other instanceof RecordIdRange)) return false;
if (this.beg?.constructor !== other.beg?.constructor) return false;
if (this.end?.constructor !== other.end?.constructor) return false;
return (
this.tb === other.tb &&
equals(this.beg?.value, other.beg?.value) &&
equals(this.end?.value, other.end?.value)
);
}

toJSON(): string {
return this.toString();
}
Expand Down
20 changes: 17 additions & 3 deletions src/data/types/recordid.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { SurrealDbError } from "../../errors";
import { equals } from "../../util/equals";
import { toSurrealqlString } from "../../util/to-surrealql-string";
import { Value } from "../value";
import { Uuid } from "./uuid";

const MAX_i64 = 9223372036854775807n;
Expand All @@ -11,11 +13,13 @@ export type RecordIdValue =
| unknown[]
| Record<string, unknown>;

export class RecordId<Tb extends string = string> {
export class RecordId<Tb extends string = string> extends Value {
public readonly tb: Tb;
public readonly id: RecordIdValue;

constructor(tb: Tb, id: RecordIdValue) {
super();

if (typeof tb !== "string")
throw new SurrealDbError("TB part is not valid");
if (!isValidIdPart(id)) throw new SurrealDbError("ID part is not valid");
Expand All @@ -24,6 +28,11 @@ export class RecordId<Tb extends string = string> {
this.id = id;
}

equals(other: unknown): boolean {
if (!(other instanceof RecordId)) return false;
return this.tb === other.tb && equals(this.id, other.id);
}

toJSON(): string {
return this.toString();
}
Expand All @@ -35,16 +44,21 @@ export class RecordId<Tb extends string = string> {
}
}

export class StringRecordId {
export class StringRecordId extends Value {
public readonly rid: string;

constructor(rid: string) {
super();
if (typeof rid !== "string")
throw new SurrealDbError("String Record ID must be a string");

this.rid = rid;
}

equals(other: unknown): boolean {
if (!(other instanceof StringRecordId)) return false;
return this.rid === other.rid;
}

toJSON(): string {
return this.rid;
}
Expand Down
9 changes: 8 additions & 1 deletion src/data/types/table.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { SurrealDbError } from "../../errors";
import { Value } from "../value";

export class Table<Tb extends string = string> {
export class Table<Tb extends string = string> extends Value {
public readonly tb: Tb;

constructor(tb: Tb) {
super();
if (typeof tb !== "string")
throw new SurrealDbError("Table must be a string");
this.tb = tb;
}

equals(other: unknown): boolean {
if (!(other instanceof Table)) return false;
return this.tb === other.tb;
}

toJSON(): string {
return this.tb;
}
Expand Down
10 changes: 9 additions & 1 deletion src/data/types/uuid.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { UUID, uuidv4obj, uuidv7obj } from "uuidv7";
import { Value } from "../value";

export class Uuid {
export class Uuid extends Value {
private readonly inner: UUID;

constructor(uuid: string | ArrayBuffer | Uint8Array | Uuid | UUID) {
super();

if (uuid instanceof ArrayBuffer) {
this.inner = UUID.ofInner(new Uint8Array(uuid));
} else if (uuid instanceof Uint8Array) {
Expand All @@ -17,6 +20,11 @@ export class Uuid {
}
}

equals(other: unknown): boolean {
if (!(other instanceof Uuid)) return false;
return this.inner.equals(other.inner);
}

toString(): string {
return this.inner.toString();
}
Expand Down
16 changes: 16 additions & 0 deletions src/data/value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export abstract class Value {
/**
* Compare equality with another value.
*/
abstract equals(other: unknown): boolean;

/**
* Convert this value to a serializable string
*/
abstract toJSON(): unknown;

/**
* Convert this value to a string representation
*/
abstract toString(): string;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from "./cbor/error";
export * from "./data";
export * from "./errors.ts";
export * from "./types.ts";
export * from "./util/equals.ts";
export * from "./util/jsonify.ts";
export * from "./util/version-check.ts";
export * from "./util/get-incremental-id.ts";
Expand Down
38 changes: 38 additions & 0 deletions src/util/equals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Value } from "../data/value";

/**
* Compares two values for deep equality, including arrays, objects,
* and SurrealQL value types.
*
* @param x The first value to compare
* @param y The second value to compare
* @returns Whether the two values are recursively equal
*/
export function equals(x: unknown, y: unknown): boolean {
if (Object.is(x, y)) return true;
if (x instanceof Date && y instanceof Date) {
return x.getTime() === y.getTime();
}
if (x instanceof RegExp && y instanceof RegExp) {
return x.toString() === y.toString();
}
if (x instanceof Value && y instanceof Value) {
return x.equals(y);
}
if (
typeof x !== "object" ||
x === null ||
typeof y !== "object" ||
y === null
) {
return false;
}
const keysX = Reflect.ownKeys(x as unknown as object) as (keyof typeof x)[];
const keysY = Reflect.ownKeys(y as unknown as object);
if (keysX.length !== keysY.length) return false;
for (let i = 0; i < keysX.length; i++) {
if (!Reflect.has(y as unknown as object, keysX[i])) return false;
if (!equals(x[keysX[i]], y[keysX[i]])) return false;
}
return true;
}
Loading

0 comments on commit eeddc8c

Please sign in to comment.