Skip to content

Commit

Permalink
feat: adds and tests a nearestCommonAncestor function
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerntannern committed Dec 26, 2019
1 parent 4ac28ff commit b084579
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 2 deletions.
23 changes: 23 additions & 0 deletions src/proto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,26 @@ export const getProtoChain = (obj: object, currentChain: object[] = []): object[

return getProtoChain(proto, [...currentChain, proto]);
};

/**
* Identifies the nearest ancestor common to all the given objects in their prototype chains. For most unrelated
* objects, this function should return Object.
*/
export const nearestCommonAncestor = (...objs: object[]): object => {
if (objs.length === 0) return undefined;

let commonAncestorProto = undefined;
const protoChains = objs.map(obj => getProtoChain(obj));

while (protoChains.every(protoChain => protoChain.length > 0)) {
const protos = protoChains.map(protoChain => protoChain.pop());
const potentialCommonAncestor = protos[0];

if (protos.every(proto => proto === potentialCommonAncestor))
commonAncestorProto = potentialCommonAncestor;
else
break;
}

return commonAncestorProto ? commonAncestorProto.constructor : commonAncestorProto;
};
54 changes: 52 additions & 2 deletions test/unit/proto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'mocha';
import { expect } from 'chai';

import { getProtoChain } from '../../src/proto';
import { getProtoChain, nearestCommonAncestor } from '../../src/proto';

describe('getProtoChain()', () => {
describe('getProtoChain', () => {
it('should return an empty list for Object.prototype', () => {
expect(getProtoChain(Object.prototype)).to.deep.equal([]);
});
Expand All @@ -26,3 +26,53 @@ describe('getProtoChain()', () => {
expect(getProtoChain(new Baz())).to.deep.equal([Baz.prototype, Bar.prototype, Foo.prototype, Object.prototype]);
});
});

describe('nearestCommonAncestor', () => {
it('should return undefined when no objects are passed', () => {
expect(nearestCommonAncestor()).to.equal(undefined);
});

it('should return undefined when objects share no common lineage (not even Object)', () => {
const a = Object.create(null);
const b = {};

expect(nearestCommonAncestor(a, b)).to.equal(undefined);
});

it('should return Object for two instances of unrelated classes', () => {
class A {}
class B {}

expect(nearestCommonAncestor(new A(), new B())).to.equal(Object);
});

it('should properly identify the common ancestor 1 layer deep', () => {
class Common {}
class A extends Common {}
class B extends Common {}

expect(nearestCommonAncestor(new A(), new B())).to.equal(Common);
});

it('should properly identify the common ancestor N layers deep', () => {
class Common {}
class A extends Common {}
class AA extends A {}
class AAA extends AA {}
class B extends Common {}
class BB extends B {}
class BBB extends BB {}

expect(nearestCommonAncestor(new AAA(), new BBB())).to.equal(Common);
});

it('should identify the common ancestor when two objects share a near ancestor, but the third does not', () => {
class ActualCommon {}
class A extends ActualCommon {}
class AlmostCommon extends ActualCommon {}
class B extends AlmostCommon {}
class C extends AlmostCommon {}

expect(nearestCommonAncestor(new A(), new B(), new C())).to.equal(ActualCommon);
});
});

0 comments on commit b084579

Please sign in to comment.