1.0.2 • Published 5 years ago

@wonderlandlabs/n v1.0.2

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

Numb

This is a DSL for numeric validation. Tired of writing verbose branchy logic for numbers, and hacky code to fix wierd values?

Don't you wish there were some sort of lodash currying system for numbers?

N is that. it is a compact(<10kb) library that helps you sanitize and transform numbers in a quick, compact manner.

Awesome formula currying

Currying _N calls always accumulates the result.

const a = _N('20');
const b = _N(30);
const c = _N(5);

console.log( 'quadratic formula part one',
  b.negate()
    .minus(
      b.sq()
        .minus(a
            .times(4)
            .times(c)
          )
          .sqrt()
    )
  .div(a
      .times(2)
    ).value
);
// quadratic formula part one -1.30901699
console.log( 'quadratic formula part two',
  b.negate()
      .plus( //<-- the difference
        b.sq()
            .minus(a
                .times(4)
                .times(c)
              )
            .sqrt()
      )
      .div(a
        .times(2)
      ).value
);
// 'quadratic formula part two', -0.191

constructor

Numb takes a value and an optional hook for fixing bad data. It also accepts a synchronous function that will be caught.

_N extracts nested values from other _N's. _N's result is immutable.

It can create a valid value with:

  • a number
  • a string that parseFloat can make a number
  • a function that returns a number
  • an invalid value with a valid substitute;
console.log(_N(3).value);
// 3
console.log(_N('3').value);
// 3 (coerced to number)
console.log(_N(_N(4)).value);
// 4

function divide(a, b) {
    if (b === 0) throw new Error('divide by zero');
    if (!_N(a).isValid || !_N(b).isValid) {
      throw new Error('bad arguments');
    }
    return a / b;
}

console.log(_N(() => divide(0, 0), 0).value)
// 0

console.log(_N(
    () => divide(0, 'string'),
    ({ message }) => {
      if (message === 'bad arguments') return -1;
      if (message === 'divide by zero') return 0;
      return null;
    }).value
)
// -1

firstGood(...) static method

_N().firstGood('string', [NaN, null, {a: 'foo', b: 100}, 2, 4], 4, NaN)

will find the first good value (2 in this case) in a flattened list of all arguments. firstGood is a "static like" method - it ignores the value of the initial _N(..).

reflection

the value input to _N is returned as the .value of the result. note, strings that are praseable by Number.parseFloat() are automatically "numberfield".

The original input is saved in the .source property.

console.log(_N(2).value === 2 ? 'is two' : 'is not two');
// 'is two'
console.log(_N('2').value === 2 ? 'is two' : 'is not two');
// 'is two'
console.log(typeof(_N('2').value));
// 'number'
console.log(_N('bad data').value === Number.NaN ? 'is NaN' : 'is not NaN');
// 'is NaN'
console.log(_N('bad data').source === 'bad data' ? 'is bad data' : 'is not bad data');
// 'is bad data'

tests - unary

The result of basic tests return one of three results:

  • true if the test is positive
  • false if the test is negative
  • null if the target value is ifInvalid

note, they are properties, not methods/functions

isPositive

isNegative

isZero

isInfinte

isInfiniteNeg

isWhole

validation tests never return null.

  • true if the test is positive
  • false if the test is negative

isInvalid

isValid

import _N from '@wonderlandlabs/numb';

console.log(_N(10).isPositive); 
// true
console.log(_N(0).isPositive); 
// false
console.log(_N('string').isPositive); 
// null

console.log(_N(-10).isNegative); 
// true
console.log(_N(0).isNegative); 
// false
console.log(_N('string').isNegative); 
// null

console.log(_N(0).isZero); 
// true
console.log(_N(10).isZero); 
// false
console.log(_N('string').isZero); 
// null

console.log(_N(100).isInfiite);
// false
console.log(_N(Number.POSITIVE_INFINITY).isInfiite);
// true

Not very interesting; where the utility comes in is with the validation methods:

tests - functional

Tests which require a comparator;

isGTE(value)

isGT(value)

isLT(value)

isLTE(value)

isEq(value)

isMult(divisor)

note that ifInvalid input or comparator returns null; null and false are both "falsy" so take care to evaluate return value carefully.

console.log(_N(2).isEq(4));
// false
console.log(_N(2).isEq(2));
// true;
console.log(_N('string').isEq(2));
// null;
console.log(_N(2).isEq('string'));
// null;

Fixing Bad Data

If you want to patch bad data, call the .fix(valueOrFn) method. If you pass a function, its response (to the original input) will be substituted for the source value.

console.log(_N(2).fix(0).value);
// 2
console.log(_N('string').fix(0).value)
// 0
console.log(_N([2]).fix((value) => Array.isArray(value) ? value[0] : 0).value);
// 2
console.log(_N(3).fix((value) => Array.isArray(value) ? value[0] : 0).value);
// 3

transformers

Transformer methods return a new Numb with the result of common number transform calculations. They all return new _N instances; so require .value suffixes to get value. The good news is they can be chained.

Unary transformers

sq()

sqrt(abs)

log()

log10()

if (abs === true) then sqrt of -4 is -2;

abs()

absN()

pi()

always negative

ceil()

floor()

negate() *= -1

rad

degrees to radians

deg

radians to degrees

Binary transformers:

add(n)

sub(n)

plus(n)

minus(n)

times(n)

div(n)

pow(n)

Trigonometers:

sin(isDeg)

cos(isDeg)

tan(isDeg)

arcSin(toDeg)

arcCos(toDeg)

arcTan(toDeg)

Parameteric transformers

clamp(limit, limit2) | clamp(limit, limit2)

clamp will return a value between the two limits, regardless of order

min(...values)

max(...values)

sum(...value)

min max and sum include the n's current value

minS(...values)

maxS(...values)

sumS(...values)

minS, maxS, sumS ignore the _N's current value;

Transformers should never throw -- worst case, they result in NaN.

console.log(
    _N(
      _N().firstGood('a', 2, 'b'),
    ).max(20)
      .times(4)
      .plus(1)
      .sqrt()
      .value
    );
// 9

Fork functions

While you can use these methods in branching logic, its more useful to listen to multiple branches defined by a test. The validation methods have the same signature:

  • test(ifTrue, [? if false, [? if ifInvalid]])

ifValid(ifTrue, ifFalse)

ifPositive(ifTrue, ifFalse, ifInvalid)

ifNegative(ifTrue, ifFalse, ifInvalid)

ifZero(ifTrue, ifFalse, ifInvalid)

ifInfinite(ifTrue, ifFalse, ifInvalid)

ifInfiniteNeg(ifTrue, ifFalse, ifInvalid)

  • the ifTrue and ifFalse handler is passed the processed (ifValid value).
  • the ifInvalid method is passed as a parameter the original unprocessed (ifInvalid) value.
const user = {
   hasArticles : false,
   username: 'fred'
}

const articles = await articleAPI.get(user.userName);

_N(_.get(articles, 'length'))
.ifValid((l) =>{
	user.hasArticles = true; 
	u.articles = articles;
	});

// or for a binary treatment:
_N(_.get(articles, 'length'))
.ifValid((l) =>{
	user.hasArticles = true; 
	u.articles = articles;
	}, 
	() => {
      user.hasArticles = false;
	  user.articles = [];
	});

else() chaining

Alternately you can call an else(fn) hook to trigger when the test fails. Note, the else clause will also trigger on ifInvalid data.

const user = {
   hasArticles : false,
   username: 'fred'
}

const articles = await articleAPI.get(user.userName);

_N(_.get(articles, 'length', 'no length'))
.ifValid((l) =>{
	user.hasArticles = true; 
	u.articles = articles;
	})
.else(
	() => {
	  user.hasArticles = false;
	  user.articles = [];
	});

.else(fn?) will "short circuit" if the initial test is true. if the first test is false, any function in the else parameter is called with the value, and additional tests will execute

let called = '';

 _N(-2)
    .ifInvalid((n) => {
      called = `${n} is ifInvalid`;
    })
    .else((n) => called = `${n} is ifValid`)
    .ifPositive((n) => {
      called = _.trim(`${called} ${n} is positive`);
    })
    .else((n) => {
      called = _.trim(`${called} ${n} is non-positive`);
    });
console.log('called: ', called);
// -2 is ifValid -2 is non-positive;

 _N('bad value')
    .ifInvalid((n) => {
      called = `${n} is ifInvalid`;
    })
    .else((n) => called = `${n} is ifValid`)
    .ifPositive((n) => {
      called = _.trim(`${called} ${n} is positive`);
    })
    .else((n) => {
      called = _.trim(`${called} ${n} is non-positive`);
    });
console.log('called: ', called);
// 'bad value is ifInvalid'

note how neither the function in the else clause NOR the positive or subsequent else clause is triggered.

chaining is fairly complex - while extensive tests have been run, if the behavior fails to meet your expectations, find a way to write your tests without the .else() chain.