npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

matchto

v3.4.0

Published

No dependency pattern matching library, able to work with numbers, arrays, strings, Dates or objects

Downloads

27

Readme

Matchto documentation

Easy to use, typed, no dependency library which takes care of pattern matching, with appropriately typed API.

Basic matching modes:

  1. Will return first passed match: 'first' - default
  2. Will throw an error if there is more than one match: 'break'
  3. Will return last passed match: 'last'
  4. Will return all matches in array: 'all'

Possible comparison modes for each data type:

  1. string: Can compare to 'Any'*, another string or regex
  2. number: Can compare to 'Any'*, or another number (same for boolean or bigint)
  3. object: Can compare with (partial) object or 'Any'*
  4. Date: Can compare with another date, 'Any'*, string, regex or number (will use ISO string format)

*Any is a pre-made constant used for 'Dont care' option. If you leave a field in compared object as undefined, it will have the same effect.

Typing for each comparison correctly matches the possible comparsion types explained above. This library supports the use of guard conditions. The guard condition is counted as a part of the match expression, which means that 'first', 'last' or 'break' mode will treat a matched expression with failed guard as unmatched.

The library will not compare functions.

Examples (from unit tests)

Simple numerical comparison


expect(match(2).to(1, 'wrong').to(2, 'right').solve()).toBe('right');
expect(match([1, 2, 3]).to({ 'any': 2 }, 'right').to({ 'any': 4 }, 'wrong').solve()).toBe('right'); // 'any' means match any of arrays elements
expect(match([1, 2, 3]).to([Any, Any, 3], 'right').to([Any, Any, 4], 'wrong').solve()).toBe('right');
expect(match({ one: 1, two: 2, three: { four: 4 } }).to({ one: 1 }, ({ item }) => item.one).to({ one: 5 }, 'wrong').solve()).toBe(1);

String/Regex comparison


expect(match('Hello').to('Hello', 'right').to('World', 'wrong').solve()).toBe('right');
expect(match('Hello').to(Any, 'right').to('World', 'wrong').solve()).toBe('right');
expect(match('Helllllo').to(/^Hel*o$/, 'right').to('World', 'wrong').solve()).toBe('right');
expect(match('Helllllo').to(new RegExp('^Hel*o$'), 'right').to('World', 'wrong').solve()).toBe('right');

expect(match('Wooorld').to(/W.*/, 'wrong').guard(s => s.length === 3).to(/Wo*rld/, 'right').guard(s => s === 'Wooorld').solve()).toBe('right');

Date comparison


expect(match([dateO, dateR]).to({ 'any': dateR.getTime() }, 'right').to({ 'any': dateW.getTime() }, 'wrong').solve()).toBe('right');
expect(match({ date: dateR }, 'all').to({ date: /2020/ }, 'right').to({ date: /2021/ }, 'wrong').solve()).toEqual(['right']);

Example of null/undefined checks


const arrays = [
    [1, 2, 3],
    [4, 5, 6],
    null,
    undefined
];
expect(match(arrays[0]).to(undefined, '1').to([1, Any, 3], '2').solve()).toBe('2');
expect(match(arrays[1]).to(null, '1').to([4, Any, 6], '2').solve()).toBe('2');
expect(match(arrays[2]).to([1, Any, 3], '1').to(null, '2').solve()).toBe('2');
expect(match(arrays[3]).to([1, Any, 3], '1').to(undefined, '2').solve()).toBe('2');

Example of complex object matching


const testObject1: ComplexTestObject = {
    id: 1,
    name: 'Oswald',
    address: 'Prague, Charles square',
    phone: ['+420', '555 111 222'],
    dateOfBirth: new Date(1990, 0, 2),
    workStatistics: {
        isAlive: true,
        worksFromHome: false,
        jobTitle: 'Plumber',
    },
    mail: '[email protected]',
};

expect(match(testObject1).to({
    address: /Ostrava/,
    dateOfBirth: /1990/,
    workStatistics: {
        isAlive: true,
    }
}, 'wrong').to({
    address: /Prague/,
    dateOfBirth: new Date(1990, 0, 2).toISOString(),
    phone: { 'any': '+420' },
    name: 'Oswald',
    workStatistics: {
        worksFromHome: Any,
        jobTitle: /P.*/
    }
}, ({ item }) => item.id).solve()).toBe(1);

Example of utilities function pattern matching


expect(match({ a: 2, b: 3, c: 4, d: { e: 5 } }).to({
    a: mod(2, 1),
    b: less(4),
    c: moreOrEqual(1),
    d: { e: lessOrEqual(6) }
}, 'wrong').to({
    a: mod(2),
    b: less(4),
    c: moreOrEqual(1),
    d: { e: lessOrEqual(6) }
}, 'right').solve()).toBe('right');

// Pattern matching with dates enhanced by functions
expect(match(date2)
    .to(around(date3.toISOString(), date4.getTime()), 'wrong')
    .to(around(date1, date3), 'right').solve()).toBe('right');
expect(match({ d: date3 })
    .to({ d: before(date1) }, 'wrong')
    .to({ d: before(date4.getTime()) }, 'right').solve()).toBe('right');
expect(match({ d: date3 })
    .to({ d: after(date4.toISOString()) }, 'wrong')
    .to({ d: after(date1) }, 'right').solve()).toBe('right');
expect(match({ d: date3 }, 'all')
    .to({ d: beforeOrNow(date1) }, 'wrong')
    .to({ d: beforeOrNow(date4.getTime()) }, 'right')
    .to({ d: beforeOrNow(date3.toISOString()) }, 'right').solve()).toEqual(['right', 'right']);
expect(match({ d: date3 }, 'all')
    .to({ d: afterOrNow(date4) }, 'wrong')
    .to({ d: afterOrNow(date1) }, 'right')
    .to({ d: afterOrNow(date3) }, 'right').solve()).toEqual(['right', 'right']);

expect(match('Carl', 'all')
    .to(less('Adam'), 'wrong')
    .to(more('Adam'), 'right')
    .to(between('Adam', 'Zeta'), 'right')
    .solve()).toEqual(['right', 'right']);

Example of advanced pattern matching in arrays


expect(match([4, 5, { one: 7 }, 8, 7, 9])
    .to({ seek: [{ one: 7 }, 7, 8] }, 'wrong')
    .to({ seek: [5, { one: 7 }, 8] }, 'right').solve()).toBe('right');

expect(match([1, 2, 3, 4]).to({ 'last': [2, 3] }, 'wrong').to({ 'last': [3, 4] }, 'right').solve()).toBe('right');

expect(match([1, 2, { one: 7 }, 9]).to({ 'some': [2, 7] }, 'wrong').to({ 'some': [{ one: 7 }, 1] }, 'right').solve()).toBe('right');

Example of pattern matching with getting which match was found


expect(match([1, 2, 3], 'all')
    .to({ any: 2 }, ({ matched }) => matched.any)
    .to({ any: 3 }, ({ matched }) => matched.any).solve()
).toEqual([2, 3]);

Pattern matching using not() function


expect(match({ one: 1, two: 2, three: [3, 4] })
    .to({ one: 1, two: 2 }, 'wrong').not()
    .to({ three: { 'last': [4, 3] } }, 'right').not().solve()
).toBe('right');

Example of using the util function 'merge' to create new objects based on patterns

expect(match('anyString').to(Any, ({ item, matched }) => merge(item, matched)).solve()).toBe('anyString');
expect(match(1).to(Any, ({ item, matched }) => merge(item, matched)).solve()).toBe(1);

expect(match({
    one: 1,
    two: [5, 6, 7],
    three: { four: [1], five: { value: "6" } },
    four: { date: new Date(2020, 6, 6) },
    five: "simpleString",
    six: new Date(2020, 6, 6),
    seven: [1, 2, 3],
    eight: {
        nine: [4, 5, 6, 7],
        ten: [5, 5, 3, 2, 1],
    },
}).to({
    one: Any, // Any will be converted by merger function into whatever value was passed into it
    two: { 'last': [Any, 7] },
    three: { four: Any, five: Any },
    four: { date: new Date(2020, 6, 6) },
    five: /simple/, // <--- regex will now be converted into "simpleString" by merger function
    six: Any,
    seven: [1, Any, less(5)], // <--- less(5) will now be converted into 3 by merger function
    eight: {
        nine: { 'seek': [5, Any, 7] },
        ten: { 'some': [Any, 5, 1, Any] },
    },
}, ({ item, matched }) => merge(item, matched)).solve()).toEqual({
    one: 1,
    two: { 'last': [6, 7] },
    three: { four: [1], five: { value: "6" } },
    four: { date: new Date(2020, 6, 6) },
    five: "simpleString",
    six: new Date(2020, 6, 6),
    seven: [1, 2, 3],
    eight: {
        nine: { seek: [5, 6, 7] },
        ten: { some: [5, 5, 1, 2] }, // Not recommended to use 'some' when merging
    },
});

Example of pattern matching using end-tail recursion


test("Can use pattern matching to create factorial", () => {
    expect(
        match(5)
            .to(1, 1)
            .to(Any, ({ item, rematch }) => item * rematch(item - 1))
            .solve()
    ).toBe(120);
});
test("Can use pattern matching to create fibonacci sequence", () => {
    expect(
        match([0, 1, 5])
            .to([Any, Any, 0], ({ item }) => [item[0], item[1]])
            .to(Any, ({ item, rematch }) => rematch([item[1], item[0] + item[1], item[2] - 1]))
            .solve()
    ).toEqual([5, 8]);
});

Example of using 'cut' function for red/green cuts and defining prolog-like variables


test("Can use 'identity' to define prolog-like facts and queries", () => {
    /* Example from: http://www.learnprolognow.org/lpnpage.php?pagetype=html&pageid=lpn-htmlse44
        max(X,Y,Y)  :-  X  =<  Y,!.
        max(X,Y,X). 
        */
    expect(
        match([2, 5, 5], 'all')
            .to([id("X"), id("Y"), id("Y")]).guard(item => item[0] <= item[1]).cut()
            .to([id("X"), id("Y"), id("X")])
            .solve()
            .find(result => Boolean(result))
    ).toBe(true);
    expect(
        match([2, 3, 5], 'all')
            .to([id("X"), id("Y"), id("Y")]).guard(item => item[0] <= item[1]).cut()
            .to([id("X"), id("Y"), id("X")])
            .solve()
    ).toEqual([]);
});
test("Can still merge even when using identity", () => {
    expect(
        match([1, 2, 2])
            .to([Any, id("X"), id("X")], ({ item, matched }) => merge(item, matched))
            .solve()
    ).toEqual([1, 2, 2])
});
test("Can extract identity value", () => {
    expect(
        match([1, 2, 3, 3])
            .to([1, id("Y"), id("X"), id("X")], ({ id }) => (id("X") + id("Y")) as number)
            .solve()
    ).toBe(5);
});

Use pattern matching to do instanceof operation


class Man {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}

class Woman {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}

class Bob extends Man {
    constructor() {
        super('Bob');
    }
}

class John extends Man {
    constructor() {
        super('John');
    }
}

describe("Can match to a class by instance type", () => {
    test("Can identify and match class instances", () => {
        expect(match(new John())
            .to(Bob, false)
            .to(Woman, false)
            .to((() => ({})) as any, false)
            .to(John, true)
            .solve()).toBe(true);
        expect(match(new John())
            .to(Bob, false)
            .to(Woman, false)
            .to(Man, true)
            .solve()).toBe(true);        
    });
    test("Can identify class instances inside complex objects", () => {
        expect(match({
            one: new John(),
            two: new Bob(),
        }, 'all').to({
            one: Man,
        }, 'man').to({
            two: Woman,
        }, 'woman').to({
            two: Bob,
        }, 'Bob').to({
            one: new Man('John'),
        }, 'new').solve()).toEqual(['man', 'Bob', 'new']);
    });
    test("Can use merger function with matching over class instances", () => {
        expect(match(new John()).to(Man, ({ item, matched }) => merge(item, matched).name).solve()).toBe('John')
        expect(match({
            one: {
                two: new John(),
                three: [new Bob(), new Woman('Anne')],
            }
        }).to({
            one: {
                two: Man,
                three: [Bob, Any],
            }
        }, ({ item, matched }) => {
            const merged = merge(item, matched);
            return [merged.one.two.name, merged.one.three[0].name, merged.one.three[1].name];
        }).solve()).toEqual(['John', 'Bob', 'Anne']);
    });
});

Can match based on primitive values by using constructors


type Arbitrary = { value: string | number };
const test: Arbitrary = { value: 5 };
expect(match(test, 'all')
    .to({ value: Number }, '1')
    .to({ value: String }, '2')
    .to({ value: 5 }, '3')
    .solve()).toEqual(['1', '3']);

Can merge item with matched value by using constructors


const date = new Date();
expect(
    match({
        str: '1',
        bool: true,
        num: 1,
        date,
        object: {},
        array: [],
    }).to({
        str: String,
        bool: Boolean,
        num: Number,
        date: Date,
        object: Object,
        array: Array,
    }, ({ item, matched }) => merge(item, matched)).solve()
).toEqual({
    str: '1',
    bool: true,
    num: 1,
    date,
    object: {},
    array: [],
});

Can use plugins to further customize pattern matching -- CAUTION: EXPERIMENTAL, USE AT OWN RISK

If a plugin function returns true, match is valid. If false, invalid. If undefined, match continues as-is.


expect(match({
    a: 5,
    b: 7,
    c: {
        d: [1],
    }
}, "first", [(a, b) => (Array.isArray(a) && Array.isArray(b) ? true : undefined)]).to({ c: { d: [5] } }).solve()).toBeTruthy();

Can use functors as objects during match

If you create a functor with enumerable properties, it will match as if it was an object


const f1 = functorFactory(() => 5, { a: 5, b: 6 }); // creates a functor with properties a: 5 and b: 6
const f2 = functorFactory(() => 6, { b: 6 });
const f3 = functorFactory(() => 7, { b: 7 });
expect(match(f1).to(f2).solve()).toBeTruthy();
expect(match(f3).to(f2).solve()).toBeFalsy();

Footer

If you notice any bugs or errors, do not hesitate to create an issue or a pull request!