crystalline
v0.2.3
Published
A modern utility library with a strong emphasis on readability. Make your code crystal clear.
Downloads
4
Maintainers
Readme
Crystalline
A modern utility library with a strong emphasis on readability. Make your code crystal clear.
Inspired by Jest Matchers.
Table of Contents
Introduction
The problem
Manipulating data is part and parcel of developing software, but the resulting code can quickly become difficult to read. You want to minimise the complexity of your codebase to ensure it's doing what you intended and have the confidence to make changes in the future.
The solution
Jest Matchers help make your tests easier to reason about. Crystalline takes this approach and applies it to your application code. It is a library of highly comprehensible functions that perform operations commonly found in code.
Guiding Principles
- Readable code is maintainable code.
- Write code as if you were writing a sentence.
- Don't reinvent the wheel when a readable native solution already exists.
- Favour brevity but not at the expense of readability.
Installation
NPM:
npm install crystalline
Yarn:
yarn add crystalline
Usage
Import specific modules to reduce the size of your bundle:
// ECMAScript modules
import { sort } from 'crystalline/arrays/sort';
// CommonJS
const { sort } = require('crystalline/arrays/sort');
sort(...);
Alternatively you can import the whole library:
// ECMAScript modules
import crystalline from 'crystalline';
// CommonJS
const crystalline = require('crystalline').default;
crystalline.arrays.sort(...);
API Reference
The library organises its functions into categories based on the type of variable they primarily operate on. All functions within a category expect that type of variable as their first parameter. Functions are always pure. Vocabulary is reused across categories to reduce the learning curve.
arrays
alter
const input = ["a", "b", "c", "d"]; const result = alter(input) .byApplying((n) => n.toUpperCase()) .atIndex(1); expect(result).toEqual(["a", "B", "c", "d"]);
const input = ["b", "n", "n", "s"]; const result = alter(input).byInsertingBetweenEachItem("a"); expect(result).toEqual(["b", "a", "n", "a", "n", "a", "s"]);
const input = ["a", "b", "c", "d", "e", "f"]; const result = alter(input).byMovingItemAtIndex(0).toIndex(2); expect(result).toEqual(["b", "c", "a", "d", "e", "f"]);
const input = ["a", "b", "c", "d", "e", "f"]; const result = alter(input).byMovingItemAtIndex(2).toTheStart(); expect(result).toEqual(["c", "a", "b", "d", "e", "f"]);
const input = ["a", "b", "c", "d", "e", "f"]; const result = alter(input).byMovingItemAtIndex(2).toTheEnd(); expect(result).toEqual(["a", "b", "d", "e", "f", "c"]);
const input1 = [1, 1, 2, 1]; const input2 = [1, "1"]; const input3 = [[42], [42]]; const result1 = alter(input1).byRemovingDuplicates(); const result2 = alter(input2).byRemovingDuplicates(); const result3 = alter(input3).byRemovingDuplicates(); expect(result1).toEqual([1, 2]); expect(result2).toEqual([1, "1"]); expect(result3).toEqual([[42]]);
const input = [1, 2, 3, 4, 5, 6, 7, 8]; const result = alter(input).byRemovingItemsBetweenIndex(2).andIndex(3); expect(result).toEqual([1, 2, 6, 7, 8]);
const input = [1, 2, 3, 4, 5, 6, 7, 8]; const result = alter(input).byRemovingItemsBetweenIndex(3).andTheEnd(); expect(result).toEqual([1, 2, 3]);
const input = [1, 2, 1, 3, 4]; const result = alter(input).byRemovingItemsEqualTo(1, 2); expect(result).toEqual([3, 4]);
const input = [ "a", false, "b", null, "c", undefined, "d", 0, "e", -0, "f", NaN, "g", "", ]; const result = alter(input).byRemovingFalsyItems(); expect(result).toEqual(["a", "b", "c", "d", "e", "f", "g"]);
findItemsIn
const input1 = [1, 2, 3, 4]; const input2 = [7, 6, 5, 4, 3]; const result = findItemsIn(input1).containedIn(input2); expect(result).toEqual([3, 4]);
const input1 = [1, 2, 3, 4]; const input2 = [7, 6, 5, 4, 3]; const result = findItemsIn(input1).notContainedIn(input2); expect(result).toEqual([1, 2]);
const input1a = [1, 2, 3, 4]; const input1b = [7, 6, 5, 4, 3]; const result = findItemsIn(input1a).and(input1b).thatAreUnique(); expect(result).toEqual([1, 2, 7, 6, 5]);
from
const input = ["foo", "bar", "baz"]; const result = from(input).pickQuantity(2).fromTheStart(); expect(result).toEqual(["foo", "bar"]);
const input = ["foo", "bar", "baz"]; const result = from(input).pickQuantity(2).fromTheEnd(); expect(result).toEqual(["bar", "baz"]);
const input = [1, 2, 3, 4, 3, 2, 1]; const result = from(input) .pickWhile((n) => n !== 4) .fromTheStart(); expect(result).toEqual([1, 2, 3]);
const input = [1, 2, 3, 4, 3, 2, 1]; const result = from(input) .pickWhile((n) => n !== 4) .fromTheEnd(); expect(result).toEqual([3, 2, 1]);
const result = from(["fi", "fo", "fum"]).pickFirst(); expect(result).toBe("fi");
const result = from(["fi", "fo", "fum"]).pickLast(); expect(result).toBe("fum");
const input = ["foo", "bar", "baz"]; const result = from(input).dropQuantity(2).fromTheStart(); expect(result).toEqual(["baz"]);
const input = ["foo", "bar", "baz"]; const result = from(input).dropQuantity(2).fromTheEnd(); expect(result).toEqual(["foo"]);
const input = [1, 2, 3, 4, 3, 2, 1]; const result = from(input) .dropWhile((n) => n <= 2) .fromTheStart(); expect(result).toEqual([3, 4, 3, 2, 1]);
const input = [1, 2, 3, 4, 3, 2, 1]; const result = from(input) .dropWhile((n) => n <= 3) .fromTheEnd(); expect(result).toEqual([1, 2, 3, 4]);
const result = from(["fi", "fo", "fum"]).dropFirst(); expect(result).toEqual(["fo", "fum"]);
const result = from(["fi", "fo", "fum"]).dropLast(); expect(result).toEqual(["fi", "fo"]);
const input = [1, 1, 1, 2, 3, 4, 4, 2, 2]; const result = from(input).dropConsecutiveRepeats(); expect(result).toEqual([1, 2, 3, 4, 2]);
const input = [1, -1, 1, 3, 4, -4, -4, -5, 5, 3, 3]; const result = from(input).dropConsecutiveRepeatsSatisfying( (x, y) => Math.abs(x) === Math.abs(y) ); expect(result).toEqual([1, 3, 4, -5, 3]);
sort
const input = [ { name: "Emma", age: 70 }, { name: "Peter", age: 78 }, { name: "Mikhail", age: 62 }, ]; const result = sort(input).ascendingByProperty("age"); expect(result).toEqual([ { name: "Mikhail", age: 62 }, { name: "Emma", age: 70 }, { name: "Peter", age: 78 }, ]);
const input = [ { name: "Emma", age: 70 }, { name: "Peter", age: 78 }, { name: "Mikhail", age: 62 }, ]; const result = sort(input).descendingByProperty("age"); expect(result).toEqual([ { name: "Peter", age: 78 }, { name: "Emma", age: 70 }, { name: "Mikhail", age: 62 }, ]);
const alice = { name: "alice", age: 40, }; const bob = { name: "bob", age: 30, }; const clara = { name: "clara", age: 40, }; const input = [alice, bob, clara]; const result = sort(input) .firstAscendingByProperty("age") .thenAscendingByProperty("name"); expect(result).toEqual([bob, alice, clara]);
const alice = { name: "alice", age: 40, }; const bob = { name: "bob", age: 30, ; const clara = { name: "clara", age: 40, }; const input = [clara, bob, alice]; const result = sort(input) .firstAscendingByProperty("age") .thenDescendingByProperty("name"); expect(result).toEqual([bob, clara, alice]);
const alice = { name: "alice", age: 40, }; const bob = { name: "bob", age: 30, }; const clara = { name: "clara", age: 40, }; const input = [clara, bob, alice]; const result = sort(input) .firstDescendingByProperty("age") .thenAscendingByProperty("name"); expect(result).toEqual([alice, clara, bob]);
const alice = { name: "alice", age: 40, }; const bob = { name: "bob", age: 30, }; const clara = { name: "clara", age: 40, }; const input = [clara, bob, alice]; const result = sort(input) .firstDescendingByProperty("age") .thenDescendingByProperty("name"); expect(result).toEqual([clara, alice, bob]);
split
const input = [1, 2, 3, 1, 2, 3]; const result = split(input).atFirstEncounterOf((n) => n === 2); expect(result).toEqual([[1], [2, 3, 1, 2, 3]]);
const input = [1, 2, 3]; const result = split(input).atIndex(1); expect(result).toEqual([[1], [2, 3]]);
const input = ["sss", "ttt", "foo", "bars"]; const result = split(input).byItemsSatisfying((n) => n.includes("s")); expect(result).toEqual([ ["sss", "bars"], ["ttt", "foo"], ]);
const input = [1, 2, 3, 4, 5, 6, 7]; const result = split(input).everyNthIndex(3); expect(result).toEqual([[1, 2, 3], [4, 5, 6], [7]]);
tally
const input = [1.0, 1.1, 1.2, 2.0, 3.0, 2.2]; const result = tally(input).byApplying(Math.floor); expect(result).toEqual({ 1: 3, 2: 2, 3: 1 });
objects
alter
const input = { firstName: " Tomato ", data: { elapsed: 100, remaining: 1400 }, id: 123, }; const result = alter(input) .byApplying((n) => n.trim()) .toKey("firstName"); expect(result).toEqual({ firstName: "Tomato", data: { elapsed: 100, remaining: 1400 }, id: 123, });
copy
const input = { a: [1, 2, 3], b: "foo", c: { c1: 123, }, }; const result = copy(input).deeply(); expect(input).toEqual(result); // Referential checks expect(input !== result).toBe(true); expect(input.a !== result.a).toBe(true); expect(input.c !== result.c).toBe(true);
const input = { a: 1, b: 2, c: 3, d: 4 }; const result = copy(input).discardKeys("a", "d"); expect(result).toEqual({ b: 2, c: 3 });
const input = { a: 1, b: 2, c: 3, d: 4 }; const result = copy(input).keepKeys("a", "c"); expect(result).toEqual({ a: 1, c: 3 });
merge
const obj1 = { name: "fred", age: 10, contact: { email: "[email protected]" }, }; const obj2 = { age: 40, hair: "blonde", contact: { email: "[email protected]" }, }; const result = merge(obj1) .deeplyWith(obj2) .resolvingConflictsViaFirstObject(); expect(result).toEqual({ name: "fred", age: 10, hair: "blonde", contact: { email: "[email protected]" }, });
const obj1 = { name: "fred", age: 10, contact: { email: "[email protected]" }, }; const obj2 = { age: 40, hair: "blonde", contact: { email: "[email protected]" }, }; const result = merge(obj1) .deeplyWith(obj2) .resolvingConflictsViaSecondObject(); expect(result).toEqual({ name: "fred", age: 40, hair: "blonde", contact: { email: "[email protected]" }, });
const obj1 = { a: true, c: { values: [10, 20] } }; const obj2 = { b: true, c: { values: [15, 35] } }; const result = merge(obj1) .deeplyWith(obj2) .resolvingConflictsByApplying((x, y) => [...x, ...y]); expect(result).toEqual({ a: true, b: true, c: { values: [10, 20, 15, 35] }, });
numbers
clamp
expect(clamp(-5).between(1, 10)).toBe(1); expect(clamp(15).between(1, 10)).toBe(10); expect(clamp(4).between(1, 10)).toBe(4);
misc
sequenceFrom
:warning: Ensure your rule function is pure and terminator condition will always be met, otherwise you risk creating an infinite loop.
const rule = (n: number) => Math.pow(n, 2); const terminator = (n: number) => n > 1e10; const seed = 10; const result = sequenceFrom(rule) .startingWith(seed) .untilCondition(terminator); expect(result).toEqual([10, 100, 10000, 100000000]);
Contributing
Thank you for thinking about contributing to Crystalline, we welcome all feedback and collaboration from the community. We don't want the process to be laborious, so we've kept our contributing guide reeeeally short. Please take a moment to read through it as doing so will help ensure the library remains consistent as it grows.