1.0.6 • Published 3 years ago

conditioner-is-better v1.0.6

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

Conditioner Is Better

A simple, powerful, and relatively fast, condition matching DSL / Array search. Abstracted from the rules engine of DarkMatterGame

Inspired by https://github.com/deitch/searchjs , ConditionerIsBetter supports most of the basic functionality of that library, and a similar/simpler DSL - But is at least 100-1000x faster (Can't remember exactly, I did the benchmark awhile back while writing this code to replace the use of searchjs validating actions and rules in the game engine ^, as it was a huge performance bottleneck)

Library consists of 2 simple functions, where and matchesConditions

Both take an array of objects, which specify conditions via one of 3 top level predicates as keys {all, any, none}, i.e. [{all: {name: 'tupac'}}] will return true for an object such as {name: tupac}

(See the full examples from the specs below for more detailed use cases)

#matchesConditions

Given the data below

import {matchesConditions} from 'conditioner-is-better';

subject = {
  name: 'tupac',
  traits: ['poet', 'warrior'],
  targets: [ {type: 'character', attack: 1}, {type: 'character', attack: 2} ],
  zone: 'units',
  attack: 10
}

any

conditions = [{ any: { zone: ['units', 'structures'] }}];
expect(matchesConditions(subject, conditions)).toEqual(true);

all

conditions = [{all: {zone: 'units', traits: 'poet'}}]
expect(matchesConditions(subject, conditions)).toEqual(true);

none

conditions = [
  {none: {traits: ['poet']}},
]
expect(matchesConditions(subject, conditions)).toEqual(false);

If the all or any condition fails here, it will return false (implicitly an all condition when there is an array of multiple conditions at the top level)

conditions = [
  {all: {zone: 'units', traits: 'poet'}},
  {any: {name: 'tupac'}}
]
expect(matchesConditions(subject, conditions)).toEqual(true);

Changing the implicit top level all, to an any condition can be accomplished as follows

conditions = {
    any: [
      {all: {zone: 'notunits', traits: 'poet'}},
      {any: {name: ['biggie', 'tupac']}}
    ]
}
expect(matchesConditions(subject, conditions)).toEqual(true);

Numeric Conditions

supports gt, lt, gte, lte

conditions = [
  {all: {attack: {gt: 5}}}
]
expect(matchesConditions(subject, conditions)).toEqual(true);

String matching Conditions

supports includes, doesNotInclude, startsWith, endsWith, doesNotStartWith, doesNotEndWith

conditions = [
  {all: {name: {includes: ['tu', 'pac']}}}
]
expect(matchesConditions(subject, conditions)).toEqual(true);

#where

Where works just like matchesConditions above, just on an array of objects as the input. So given the data below:

import {where} from 'conditioner-is-better';

subject = [
  {
    name: 'tupac',
    traits: ['poet', 'warrior'],
    zone: 'west',
    attack: 10
  },
  {
    name: 'biggie',
    traits: ['poet', 'dealer'],
    zone: 'east',
    attack: 5
  }
]

Return objects matching a string property

conditions = [{all: {name: 'tupac'}}]
expect(where(subject, conditions)[0].name === 'tupac').toEqual(true);

Return objects matching a string in an array

conditions = [{all: {traits: 'dealer'}}]
expect(where(subject, conditions)[0].name === 'biggie').toEqual(true);

Return objects matching a numeric constraint

conditions = [{all: {attack: {gt: 5}}}]
expect(where(subject, conditions)[0].name).toEqual('tupac');
expect(where(subject, conditions).length).toEqual(1);

So on and so forth.

matchesConditions spec full examples

import {matchesConditions} from 'conditioner-is-better';

subject = {
  name: 'tupac',
  traits: ['poet', 'warrior'],
  targets: [ {type: 'character', attack: 1}, {type: 'character', attack: 2} ],
  zone: 'units',
  attack: 10
}

conditions = [{ any: { zone: ['units', 'structures'] }}];
expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [{all: {zone: 'units', traits: 'poet'}}]
expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {zone: 'units', traits: 'poet'}},
  {any: {name: 'tupac'}}
]

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {zone: 'units', traits: 'poet'}},
  {any: {name: 'biggie'}}
]

expect(matchesConditions(subject, conditions)).toEqual(false);

conditions = [
  {all: {zone: 'units', traits: 'poet'}},
  {any: {name: ['biggie', 'tupac']}}
]

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {zone: 'notunits', traits: 'poet'}},
  {any: {name: ['biggie', 'tupac']}}
]

expect(matchesConditions(subject, conditions)).toEqual(false);

conditions = {
    any: [
      {all: {zone: 'notunits', traits: 'poet'}},
      {any: {name: ['biggie', 'tupac']}}
    ]
}

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {any: {zone: 'notunits', traits: 'poet'}},
  {any: {name: ['biggie', 'tupac']}}
]

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {traits: 'poet'}},
  {all: {traits: 'warrior'}}
]

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {traits: ['poet', 'warrior']}}
]

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {traits: ['poet']}},
  {all: {name: 'tupac', zone: 'units'}}
]

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {traits: ['poet']}},
  {all: {name: 'tupac', zone: 'junkyard'}}
]

expect(matchesConditions(subject, conditions)).toEqual(false);

conditions = [
  {none: {traits: ['poet']}},
]

expect(matchesConditions(subject, conditions)).toEqual(false);

conditions = [
  {none: {traits: ['blah']}},
]

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {attack: 10}}
]

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {attack: {gt: 5}}}
]

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {attack: {gt: 15}}}
]

expect(matchesConditions(subject, conditions)).toEqual(false);

conditions = [
  {all: {name: {includes: 'pac'}}}
]

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {name: {includes: ['tu', 'pac']}}}
]

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {name: {includes: ['tu', 'pac']}}},
  {all: {name: {doesNotInclude: ['tup']}}}
]

expect(matchesConditions(subject, conditions)).toEqual(false);

conditions = [
  {all: {name: {startsWith: ['tu']}}}
]

expect(matchesConditions(subject, conditions)).toEqual(true);

conditions = [
  {all: {name: {endsWith: ['pac']}}}
]

expect(matchesConditions(subject, conditions)).toEqual(true);

where full spec examples

import {where} from 'conditioner-is-better';

subject = [
  {
    name: 'tupac',
    traits: ['poet', 'warrior'],
    zone: 'west',
    attack: 10
  },
  {
    name: 'biggie',
    traits: ['poet', 'dealer'],
    zone: 'east',
    attack: 5
  }
]

conditions = [{all: {name: 'tupac'}}]
expect(where(subject, conditions)[0].name === 'tupac').toEqual(true);

conditions = [{all: {traits: 'dealer'}}]
expect(where(subject, conditions)[0].name === 'biggie').toEqual(true);

conditions = [{all: {traits: 'poet'}}]
expect(where(subject, conditions).length).toEqual(2);

conditions = [{all: {attack: {gt: 5}}}]
expect(where(subject, conditions)[0].name).toEqual('tupac');
expect(where(subject, conditions).length).toEqual(1);

conditions = [{all: {attack: {gt: 5}, zone: 'east'}}]
expect(where(subject, conditions).length).toEqual(0);

conditions = [{all: {name: {includes: 'z'}}}]
expect(where(subject, conditions).length).toEqual(0);

conditions = [{all: {zone: {includes: 'e'}}}]
expect(where(subject, conditions).length).toEqual(2);

Special Directives

As shown above, when comparing numeric values, you can pass an object as the value, i.e. {all: {cost: {gt: 2}}}, which allows you to use the following directives as keys:

gt, lt, gte, lte

The same goes for strings, but the directives are:

includes, doesNotInclude, startsWith, endsWith, doesNotStartWith, doesNotEndWith

Serialization

This library was written with hardcoded javascript constants in mind, thus the succinctness of the DSL. The downside to that, is it can make building a UI and storing it in the database, a little bit awkward. To get around that, there are serialization methods that will expand/compress the query into a more UI friendly structure, for instance:

import {expandQuery, compressQuery} from 'conditioner-is-better';
q = expandQuery([{ any: { zone: ['units', 'structures'] }}])
// will yield the following
[
    {
        "conditions": [
            {
                "comparisons": [
                    {
                        "property": "zone",
                        "value": [
                            "units",
                            "structures"
                        ]
                    }
                ],
                "quantifier": "any"
            }
        ],
        "quantifier": "all"
    }
]

compressQuery(q)
//will yield back the following, which can then be ran via matchesConditions or where
{
    "all": [
        {
            "any": {
                "zone": [
                    "units",
                    "structures"
                ]
            }
        }
    ]
}

q = expandQuery([{all: { zone: 'units', traits: 'poet'}}])
// will yield the following, note the fact that all values without a special directive will be moved into an object that has a key named '='

[
    {
        "conditions": [
            {
                "comparisons": [
                    {
                        "property": "zone",
                        "value": {
                            "=": "units"
                        }
                    },
                    {
                        "property": "traits",
                        "value": {
                            "=": "poet"
                        }
                    }
                ],
                "quantifier": "all"
            }
        ],
        "quantifier": "all"
    }
]
1.0.6

3 years ago

1.0.5

3 years ago

1.0.4

3 years ago

1.0.3

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.0

3 years ago