10

I need to store a list of points and check if a new point is already included in that list

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}

window.onload = () => {
    var points : Point[] = [];
    points.push(new Point(1,1));
    var point = new Point(1,1);
    alert(points.indexOf(point)); // -1 
}

Obviously typescript uses comparison by reference but in this case that doesn't make sense. In Java or C# I would overload the equals method, in typescript that doesn't seem to be possible.

I considered to loop through the array with foreach and check each entry for equality, but that seems rather complicated and would bloat the code.

Is there something like equals in typescript ? How can I implement my own comparisons ?

lhk
  • 27,458
  • 30
  • 122
  • 201
  • 3
    Typescript is a metalanguage, not a language. You should ask "Is there something like equals in javascript", not typescript. – danludwig Jan 28 '14 at 13:10
  • Where / how are you doing the comparison in your script? – Floris Jan 28 '14 at 13:10
  • 1
    @danludwig, typescript has generics. they could have implemented custom container classes that check for something like equals. an interface icomparable and a generic class arraylist would do the job. – lhk Feb 02 '14 at 09:29
  • I'm not an expert but I think dart provides something like `hashcode` and you can override it. http://pchalin.blogspot.com/2014/04/defining-equality-and-hashcode-for-dart.html – Konrad Jun 18 '19 at 12:24
  • Might be a worthy alternative to Typescript? – Konrad Jun 18 '19 at 12:25
  • Also, frameworks like Vue provide an option to set "key" to identify objects so it's handy. See https://vuejs.org/v2/guide/list.html `v-bind:key` – Konrad Jun 18 '19 at 12:39
  • In WPF there's `SelectedValuePath` perhaps? To avoid overriding `Equals` – Konrad Jun 18 '19 at 12:43

5 Answers5

5

Typescript doesn't add any functionality to JavaScript. It's just "typed" and some syntax improvements.

So, there's not a way to override equals in an equivalent way to what you might have done in C#.

However, you would have ended up likely using a Hash or a strongly-typed Dictionary in C# to do an efficient lookup (in addition to the array potentially), rather than using an "index of" function.

For that, I'd just suggest you use an associative array structure to store the Points.

You'd do something like:

class Point {
    constructor(public x:Number = 0, 
        public y:Number = 0 ) {     
    }   
    public toIndexString(p:Point):String {
        return Point.pointToIndexString(p.x, p.y);  
    }       
    static pointToIndexString(x:Number, y:Number):String {
        return x.toString() + "@" + y.toString();   
    }       
}

var points:any = {};
var p: Point = new Point(5, 5);
points[p.toIndexString()] = p;

If a Point doesn't exist, checking the points associative array will returned undefined.

A function wrapping the array would be simple:

function findPoint(x:Number, y:Number):Point {
    return points[Point.pointToIndexString(x, y)];
}

Looping through all points would be easy to:

// define the callback (similar in concept to defining delegate in C#)
interface PointCallback {
    (p:Point):void;
}

function allPoints(callback:PointCallback):void {
    for(var k in points) {
        callback(points[k]);
    }
}

allPoints((p) => {
   // do something with a Point...
});
WiredPrairie
  • 58,954
  • 17
  • 116
  • 143
  • great, thanks for the answer. i ended up implementing a custom list type like the one in steve fentons answer. but this looks nice, too – lhk Jan 28 '14 at 22:34
  • 1
    If you're doing much searching, his solution is currently O(n). Not ideal from a perf perspective. An associative array would be super fast. – WiredPrairie Jan 28 '14 at 23:12
  • how can an associative array be faster than O(n) ? It has to check all keys, too. Instead of comparing strings he's comparing points. Correct me if i'm wrong, but i don't see the difference. – lhk Jan 29 '14 at 15:06
  • An associative array uses a hash, which often is O(1). – WiredPrairie Jan 29 '14 at 17:21
4

You could wrap the collection in a PointList that only allows Point objects to be added via an add method, which checks to ensure no duplicates are added.

This has the benefit of encapsulating the "No duplicates" rule in a single place, rather than hoping that all calling code will check before adding a duplicate, which would duplicate the rule in many places.

class Point {
    constructor(public x: number, public y: number) {
    }
}

class PointList {
    private points: Point[] = [];

    get length() {
        return this.points.length;
    }

    add(point: Point) {
        if (this.exists(point)) {
            // throw 'Duplicate point';
            return false;
        }

        this.points.push(point);
        return true;
    }

    exists(point: Point) {
        return this.findIndex(point) > -1;
    }

    findIndex(point: Point) {
        for (var i = 0; i < this.points.length; i++) {
            var existingPoint = this.points[i];
            if (existingPoint.x === point.x && existingPoint.y === point.y) {
                return i;
            }
        }

        return -1;
    }
}

var pointList = new PointList();

var pointA = new Point(1, 1);
var pointB = new Point(1, 1);
var pointC = new Point(1, 2);

pointList.add(pointA);
alert(pointList.length); // 1

pointList.add(pointB);
alert(pointList.length); // 1

pointList.add(pointC);
alert(pointList.length); // 2
Fenton
  • 241,084
  • 71
  • 387
  • 401
  • 1
    Thanks, this is great, although I would rather call this class **`PointSet`** instead of `PointList` as the functionality is exactly that of a set. But then, you'd have to change `length()` to `size()` and `exists()` to `has()` to better match the methods available for a Typescript set. – Splines Nov 06 '21 at 23:53
3

One thing you can do is try out linq.js. With that, you can do something like this:

var foundPoint = Enumerable.From(points)
    .SingleOrDefault(function(p) {
        return p.x == targetX && p.y == targety;
    });

... you could then just implement this function on your object

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
    static equals(points: Point[], candidate: Point): boolean {
        var foundPoint = Enumerable.From(points)
            .SingleOrDefault((p: Point): boolean => {
                return p.x == candidate.x && p.y == candidate.y;
             });
        return foundPoint != null;
    }
}

... and use it like this

var points = createPointsArray();
var point = getSomePoint();
// is point already in array?
var isPointInPoints = Point.equals(points, point)
danludwig
  • 46,965
  • 25
  • 159
  • 237
  • 1
    @lhk If you do go this route, DefinitelyTyped has the Typescript d.ts for linq.js: https://github.com/borisyankov/DefinitelyTyped/tree/master/linq – Jude Fisher Jan 28 '14 at 14:16
  • nice, ill definitely read up on linqjs. for now i used a custom list like the one in steve fentons answer – lhk Jan 28 '14 at 22:32
2

Solution using a Set

I thought that a Set would be a perfect fit for this kind of problem since:

A value in the Set may only occur once; it is unique in the Set's collection. — MDN

However, you can still add [0, 1, 2, 3] multiples times to a Set (see this answer) since Sets use the SameValueZero(x, y) comparison algorithm to compare values (see also this answer). That's why you have to implement your own version of a Set.

Custom Set implemenation

I ported Johnny Bueti's solution to Sets. I don't overwrite any prototype as that is considered bad practice and can have unwanted side-effects.

Inside util.ts:

export interface Equatable {
    /**
    * Returns `true` if the two objects are equal, `false` otherwise.
    */
    equals(object: any): boolean
}

export class SetCustomEquals<T extends Equatable> extends Set<T>{

    add(value: T) {
        if (!this.has(value)) {
            super.add(value);
        }
        return this;
    }

    has(otherValue: T): boolean {
        for (const value of this.values()) {
            if (otherValue.equals(value)) {
                return true;
            }
        }
        return false;
    }
}

Usage

import { Equatable, SetCustomEquals } from "./util";

class MyClass implements Equatable {

    equals(other: MyClass): boolean {
        ... (your custom code)
    }

}

const mySet= new SetCustomEquals<MyClass>();
const myObject = new MyClass();
mySet.add(myObject);
Splines
  • 550
  • 1
  • 6
  • 17
1

I would implement my own comparison method. In this instance I'm extending the Array.prototype - which I would not suggest unless you know exactly what you're doing - but you can very well create your own class that inherits from Array and implements a .contains() method like the one defined below.

interface Equatable {
  /**
  * Returns `true` if the two objects are equal, `false` otherwise.
  */
  equals(object: any): boolean
}

class Point implements Equatable {
  public equals(toPoint: Point): boolean {
    return this.x === toPoint.x && this.y === toPoint.y;
  }
}

// Augment the global scope
declare global {
  interface Array<T extends Equatable> {
    /**
    * Returns `true` if an object is found, `false` otherwise. This method uses the `.equals()` method to compare `Equatable` objects for equality.
    */
    contains(object: T): boolean
  }
}

// Extend the Array.prototype.
Array.prototype.contains = function(object: Equatable) {
  return this.findIndex(element => {
    return element.equals(object);
  }) !== -1;
}

The Equatable interface allows you to extend this behaviour to any other object:

class User implements Equatable {
  public equals(toUser: User): boolean {
    return this.uuid === toUser.uuid;
  }
}
Johnny Bueti
  • 637
  • 1
  • 8
  • 27
  • +1 interesting answer. But I'm thinking: wouldn't be better to use generics? I would expect something like: `class User implements Equatable` to be sure that equals gets called with a User object. What do you think? – Ansharja Jul 23 '22 at 11:14
  • That could definitely work, yes! I do remember having a very odd and specific issue with making `Equatable` generic, but not quite which was it. – Johnny Bueti Jul 24 '22 at 14:55