Skip to content

Commit

Permalink
Merge pull request #2 from MikeIbberson/feat/negatives
Browse files Browse the repository at this point in the history
Negative comparisons
  • Loading branch information
MikeIbberson committed Feb 3, 2020
2 parents 3d14b84 + b1c6d9a commit d9550ee
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 24 deletions.
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@
<img src='https://bettercodehub.com/edge/badge/MikeIbberson/comparisons?branch=master'>
</p>

<p>Currently, expressions support <code>=</code>, <code>>=</code>, <code><=</code>, <code>></code> and <code><</code> operators. Optionally, you can include a second constructor argument for changing the locale (the default is "en"). Any expressions that do not match a recognized operation get stripped out and are assumed to pass. For simply matching if a property exists, use the <code>=*</code> expression.</p>
<p>Currently, expressions support <code>=</code>, <code>>=</code>, <code><=</code>, <code>></code>, <code><</code> and <code>!=</code> operators. Optionally, you can include a second constructor argument for changing the locale (the default is "en"). Any expressions that do not match a recognized operation get stripped out and are assumed to pass.</p>

<p>For simply matching if a property exists, use the <code>=*</code> expression. Likewise, use the <code>!</code> flag on the key name to check for missing/empty values.</p>

<p>Don't worry about type casting&mdash;we'll handle that for you.</p>

<h2>Example usage</h2>

```Bash
# Or use npm install
yarn add comparisons
```

```Javascript
// @TODO yarn add comparisons
const Comparison = require('comparisons');

const tests = ['foo=bar', 'num>=2'];
const tests = ['foo=bar', 'num>=2', '!quuz'];
const stub = { foo: 'bar', num: 3 };
const runner = new Comparison(tests);
runner.eval(stub); // returns true or false
runner.query(); // returns a Mongo friendly query
```

<h2>Coming Soon</h2>
I'd like to implement non-matching operators into this as well so we can compare against differences.
8 changes: 7 additions & 1 deletion lib/__tests__/comparison.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ const exp = [
'language=EN',
'bestFriend.name=jon',
'colleague.age=*',
'!colour',
];

const stub = {
colour: null,
foo: 'bar',
age: 21,
language: 'en',
Expand All @@ -29,6 +31,7 @@ describe('Comparison', () => {
expect(Comparison.isValid('<')).toBeTruthy();
expect(Comparison.isValid('>=')).toBeTruthy();
expect(Comparison.isValid('<=')).toBeTruthy();
expect(Comparison.isValid('!foo')).toBeTruthy();
});
});

Expand All @@ -38,7 +41,9 @@ describe('Comparison', () => {

it('should return falsy', () =>
expect(
new Comparison(['age>22', 'language=FR']).eval(stub),
new Comparison(['age>22', 'language=FR', 'color=red']).eval(
stub,
),
).toBeFalsy());
});

Expand All @@ -52,6 +57,7 @@ describe('Comparison', () => {
{ language: /EN/gi },
{ 'bestFriend.name': /jon/gi },
{ 'colleague.age': { $exists: true } },
{ 'colour': { $or: [{ $exists: false }, { $eq: null }] } },
],
}));

Expand Down
22 changes: 22 additions & 0 deletions lib/__tests__/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ describe('Utils', () => {
expect(Utils.equals('a', '*')).toBeTruthy());
});

describe('"doesNotEqual"', () => {
it('should return truthy', () =>
expect(Utils.doesNotEqual('foo', 'bar')).toBeTruthy());

it('should return falsy', () =>
expect(Utils.doesNotEqual('foo', 'foo')).toBeFalsy());

it('should return falsy', () =>
expect(Utils.doesNotEqual(2, 2)).toBeFalsy());
});

describe('isGreaterThan', () => {
it('should return truthy on numerics', () =>
expect(Utils.isGreaterThan(123, 12)).toBeTruthy());
Expand Down Expand Up @@ -97,4 +108,15 @@ describe('Utils', () => {
it('should return truthy on equal value', () =>
expect(Utils.isGreaterThanOrEqualTo(1, 2)).toBeFalsy());
});

describe('isEmpty', () => {
it('should return truthy on null', () =>
expect(Utils.isEmpty(null)).toBeTruthy());

it('should return truthy on undefined', () =>
expect(Utils.isEmpty(undefined)).toBeTruthy());

it('should return falsy on false', () =>
expect(Utils.isEmpty(false)).toBeFalsy());
});
});
31 changes: 20 additions & 11 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ const flat = require('flat');

const {
equals,
doesNotEqual,
isGreaterThan,
isLessThan,
isGreaterThanOrEqualTo,
isLessThanOrEqualTo,
isEmpty,
cast,
} = require('./utils');

Expand All @@ -15,10 +17,12 @@ const ops = {
'>': isGreaterThan,
'<': isLessThan,
'=': equals,
'!=': doesNotEqual,
};

const getAssignment = {
'equals': cast,
'isEmpty': cast,
'isLessThan': (v) => ({
$lt: cast(v),
}),
Expand All @@ -37,18 +41,23 @@ const keys = Object.keys(ops);
const re = new RegExp(`[${keys.map((v) => `(${v})`).join('')}]`, 'i');

const hasLength = (v) => Array.isArray(v) && v.length;
const hasNegation = (v) => typeof v === 'string' && v.startsWith('!');

const getOp = (v) =>
keys.reduce(
(a, c) =>
v.includes(c) && !hasLength(a)
? v
.split(re)
.filter(Boolean)
.concat(ops[c])
: a,
[],
);
hasNegation(v)
? // reference the cast function conditionals
// this leverages how wildcards work, just the inverse
[v.replace('!', ''), '!', isEmpty]
: keys.reduce(
(a, c) =>
v.includes(c) && !hasLength(a)
? v
.split(re)
.filter(Boolean)
.concat(ops[c])
: a,
[],
);

class Comparison {
constructor(exp, locale = 'en') {
Expand All @@ -57,7 +66,7 @@ class Comparison {
}

static isValid(v) {
return re.test(v);
return hasNegation(v) || re.test(v);
}

get eligible() {
Expand Down
19 changes: 14 additions & 5 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,19 @@ class ValidatorRunner {
}
}

const runCompare = (a, b, locale) =>
String(a).localeCompare(String(b), locale, {
sensitivity: 'base',
});

const equals = (a, b, locale) => {
if (b === '*') return a !== '' && a !== undefined && a !== null;

return (
String(a).localeCompare(String(b), locale, {
sensitivity: 'base',
}) === 0
);
return runCompare(a, b, locale) === 0;
};

const doesNotEqual = (a, b, locale) => runCompare(a, b, locale) !== 0;

const isGreaterThan = (a, b) =>
new ValidatorRunner([a, b]).sequence([
'isGreaterThanNumeric',
Expand All @@ -90,19 +93,25 @@ const isLessThanOrEqualTo = (a, b, locale) =>

const cast = (v) => {
if (v === '*') return { $exists: true };
if (v === '!') return { $or: [{ $exists: false }, { $eq: null }] };

const test = validate(v);
if (test.date()) return v;
if (test.numeric()) return parseFloat(v);
return new RegExp(v, 'i');
};

const isEmpty = (v) =>
v === undefined || v === null || v === 'undefined' || v === 'null';

module.exports = {
equals,
doesNotEqual,
isGreaterThan,
isLessThan,
isGreaterThanOrEqualTo,
isLessThanOrEqualTo,
isEmpty,
validate,
cast,
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "comparisons",
"version": "2.0.3",
"version": "2.1.1",
"main": "lib/index.js",
"license": "MIT",
"jest": {
Expand Down

0 comments on commit d9550ee

Please sign in to comment.