Skip to content

💎 A modern utility library with a strong emphasis on readability.

License

Notifications You must be signed in to change notification settings

sekhavati/crystalline

Repository files navigation

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

  1. Readable code is maintainable code.
  2. Write code as if you were writing a sentence.
  3. Don't reinvent the wheel when a readable native solution already exists.
  4. 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

    byApplying

    • atIndex


      Create a new array by applying the function supplied at the given index.

      const input = ["a", "b", "c", "d"];
      
      const result = alter(input)
        .byApplying((n) => n.toUpperCase())
        .atIndex(1);
      
      expect(result).toEqual(["a", "B", "c", "d"]);

    byInsertingBetweenEachItem


    Create a new array with the value supplied inserted between each item.

    const input = ["b", "n", "n", "s"];
    
    const result = alter(input).byInsertingBetweenEachItem("a");
    
    expect(result).toEqual(["b", "a", "n", "a", "n", "a", "s"]);

    byMovingItemAtIndex

    • toIndex


      Create a new array with the item at the index specified moved to the chosen index.

      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"]);

    • toTheStart


      Create a new array with the item at the index specified moved to the start of the array.

      const input = ["a", "b", "c", "d", "e", "f"];
      
      const result = alter(input).byMovingItemAtIndex(2).toTheStart();
      
      expect(result).toEqual(["c", "a", "b", "d", "e", "f"]);

    • toTheEnd


      Create a new array with the item at the index specified moved to the end of the array.

       const input = ["a", "b", "c", "d", "e", "f"];
      
       const result = alter(input).byMovingItemAtIndex(2).toTheEnd();
      
       expect(result).toEqual(["a", "b", "d", "e", "f", "c"]);

    byRemovingDuplicates


    Create a new array with any duplicates from the original removed.

    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]]);

    byRemovingItemsBetweenIndex

    • andIndex


      Create a new array with all items between the two indexes removed.

      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]);

    • andTheEnd


      Create a new array with all items between the index specified and the end removed.

      const input = [1, 2, 3, 4, 5, 6, 7, 8];
      
      const result = alter(input).byRemovingItemsBetweenIndex(3).andTheEnd();
      
      expect(result).toEqual([1, 2, 3]);

    byRemovingItemsEqualTo


    Create a new array with any items matching those supplied removed.

    const input = [1, 2, 1, 3, 4];
    
    const result = alter(input).byRemovingItemsEqualTo(1, 2);
    
    expect(result).toEqual([3, 4]);

    byRemovingFalsyItems


    Create a new array with all falsy items removed.

    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

    containedIn


    Create a new array containing only items that are present in both the first and second array.

    const input1 = [1, 2, 3, 4];
    const input2 = [7, 6, 5, 4, 3];
    
    const result = findItemsIn(input1).containedIn(input2);
    
    expect(result).toEqual([3, 4]);

    notContainedIn


    Create a new array containing only items from the first array that are not present in second array.

    const input1 = [1, 2, 3, 4];
    const input2 = [7, 6, 5, 4, 3];
    
    const result = findItemsIn(input1).notContainedIn(input2);
    
    expect(result).toEqual([1, 2]);

    thatAreUnique


    Create a new array containing items that are only present in one of the two input arrays.

    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

    pickQuantity

    • fromTheStart


      Create a new array containing the first N number of items from the input array.

      const input = ["foo", "bar", "baz"];
      
      const result = from(input).pickQuantity(2).fromTheStart();
      
      expect(result).toEqual(["foo", "bar"]);

    • fromTheEnd


      Create a new array containing the last N number of items from the input array.

      const input = ["foo", "bar", "baz"];
      
      const result = from(input).pickQuantity(2).fromTheEnd();
      
      expect(result).toEqual(["bar", "baz"]);

    pickWhile

    • fromTheStart


      Create a new array by selecting items from the start of the input array until the predicate returns false.

       const input = [1, 2, 3, 4, 3, 2, 1];
      
       const result = from(input)
         .pickWhile((n) => n !== 4)
         .fromTheStart();
      
       expect(result).toEqual([1, 2, 3]);

    • fromTheEnd


      Create a new array by selecting items from the end of the input array until the predicate returns false.

       const input = [1, 2, 3, 4, 3, 2, 1];
      
       const result = from(input)
         .pickWhile((n) => n !== 4)
         .fromTheEnd();
      
       expect(result).toEqual([3, 2, 1]);

    pickFirst


    Return the first item from the input array.

    const result = from(["fi", "fo", "fum"]).pickFirst();
    
    expect(result).toBe("fi");

    pickLast


    Return the last item from the input array.

    const result = from(["fi", "fo", "fum"]).pickLast();
    
    expect(result).toBe("fum");

    dropQuantity

    • fromTheStart


      Create a new array containing all items from the input array with the first N items removed.

       const input = ["foo", "bar", "baz"];
      
       const result = from(input).dropQuantity(2).fromTheStart();
      
       expect(result).toEqual(["baz"]);

    • fromTheEnd


      Create a new array containing all items from the input array with the last N items removed.

      const input = ["foo", "bar", "baz"];
      
      const result = from(input).dropQuantity(2).fromTheEnd();
      
      expect(result).toEqual(["foo"]);

    dropWhile

    • fromTheStart


      Create a new array by removing items from the start of the input array until the predicate returns false.

      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]);

    • fromTheEnd


      Create a new array by removing items from the end of the input array until the predicate returns false.

      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]);

    dropFirst


    Create a new array containing every item from the input array except the first.

    const result = from(["fi", "fo", "fum"]).dropFirst();
    
    expect(result).toEqual(["fo", "fum"]);

    dropLast


    Create a new array containing every item from the input array except the last.

    const result = from(["fi", "fo", "fum"]).dropLast();
          
    expect(result).toEqual(["fi", "fo"]);

    dropConsecutiveRepeats


    Create a new array containing every item from the input array with any consecutively repeated elements removed.

    const input = [1, 1, 1, 2, 3, 4, 4, 2, 2];
    
    const result = from(input).dropConsecutiveRepeats();
    
    expect(result).toEqual([1, 2, 3, 4, 2]);

    dropConsecutiveRepeatsSatisfying


    Create a new array containing every item from the input array with any consecutive elements satisfying the predicate removed.

    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

    ascendingByProperty


    Create a new array with items from the input array sorted in ascending order by the given property.

    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 },
    ]);

    descendingByProperty


    Create a new array with items from the input array sorted in descending order by a given property.

    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 },
    ]);

    firstAscendingByProperty

    • thenAscendingByProperty


      Create a new array with items from the input array sorted in ascending order by the first property, then ascending by the second property.

      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]);

    • thenDescendingByProperty


      Create a new array with items from the input array sorted in ascending order by the first property, then descending by the second property.

      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]);

    firstDescendingByProperty

    • thenAscendingByProperty


      Create a new array with items from the input array sorted in descending order by the first property, then ascending by the second property.

      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]);

    • thenDescendingByProperty


      Create a new array with items from the input array sorted in descending order by the first property, then descending by the second property.

      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

    atFirstEncounterOf


    Create a new array that contains two arrays after splitting the original at the first point where the predicate holds true.

    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]]);

    atIndex


    Create a new array that contains two arrays after splitting the original at the index specified.

    const input = [1, 2, 3];
    
    const result = split(input).atIndex(1);
    
    expect(result).toEqual([[1], [2, 3]]);

    byItemsSatisfying


    Create a new array that contains two arrays after separating the contents of the original into items that satisfy the predicate and those that don't.

    const input = ["sss", "ttt", "foo", "bars"];
    
    const result = split(input).byItemsSatisfying((n) => n.includes("s"));
    
    expect(result).toEqual([
      ["sss", "bars"],
      ["ttt", "foo"],
    ]);

    everyNthIndex


    Create a new array that contains multiple other arrays that are the result of splitting the original every N items.

    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

    byApplying


    Create an object that contains a count of elements in an array according to how many match a key generated by the supplied function.

    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

    byApplying

    • toKey


      Create a new object that is a copy of the original but with the transformation applied to the key specified.

      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

    deeply


    Create a deep copy of the object including any nested objects.

    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);

    discardKeys


    Create a partial copy of the object omitting the keys specified.

    const input = { a: 1, b: 2, c: 3, d: 4 };
    
    const result = copy(input).discardKeys("a", "d");
    
    expect(result).toEqual({ b: 2, c: 3 });

    keepKeys


    Create a partial copy of an object containing only the keys specified.

    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

    deeplyWith

    • resolvingConflictsViaFirstObject


      Create a new object with all properties from the input objects, using values from the first object when the same keys exist in both.

      const obj1 = {
        name: "fred",
        age: 10,
        contact: { email: "moo@example.com" },
      };
      const obj2 = {
        age: 40,
        hair: "blonde",
        contact: { email: "baa@example.com" },
      };
      
      const result = merge(obj1)
        .deeplyWith(obj2)
        .resolvingConflictsViaFirstObject();
      
      expect(result).toEqual({
        name: "fred",
        age: 10,
        hair: "blonde",
        contact: { email: "moo@example.com" },
      });

    • resolvingConflictsViaSecondObject


      Create a new object with all properties from the input objects, using values from the second object when the same keys exist in both.

      const obj1 = {
        name: "fred",
        age: 10,
        contact: { email: "moo@example.com" },
      };
      const obj2 = {
        age: 40,
        hair: "blonde",
        contact: { email: "baa@example.com" },
      };
      
      const result = merge(obj1)
        .deeplyWith(obj2)
        .resolvingConflictsViaSecondObject();
      
      expect(result).toEqual({
        name: "fred",
        age: 40,
        hair: "blonde",
        contact: { email: "baa@example.com" },
      });

    • resolvingConflictsByApplying


      Create a new object with all properties from the input objects, using the resolver function to derive a value for keys that exist in both.

      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

    between


    Restrict a number to be within the range specified.

    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

    startingWith

    • untilCondition


      Create an array of items using the rule and seed value up until the terminator condition is met.

      ⚠️ 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.

Contributing Guide