Skip to content

Commit

Permalink
arraymake is ready
Browse files Browse the repository at this point in the history
  • Loading branch information
jfet97 committed Aug 7, 2019
0 parents commit 22caa7a
Show file tree
Hide file tree
Showing 10 changed files with 6,626 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .editorconfig
@@ -0,0 +1,11 @@
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

# editorconfig-tools is unable to ignore longs strings or urls
max_line_length = 120
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
node_modules
dist/
coverage/
21 changes: 21 additions & 0 deletions LICENSE
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2019 Andrea Simone Costa

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
105 changes: 105 additions & 0 deletions README.md
@@ -0,0 +1,105 @@
# arraymake

## introduction

A new ES3 compliant static method for the Array constructor that let you create an array with predefined length and filling values.

```js
Array.make(3, 42); // [42, 42, 42]

Array.make(3, (_, i) => i); // [0, 1, 2]

Array.make(3, () => ({ safe: true })); // [{ safe: true }, { safe: true }, { safe: true }]
```

## installation

Install it from __npm__:
```sh
$ npm i -S arraymake
```

then import it:
```js
import "arraymake";

require("arraymake");
```

## motivation

Currently there isn't a way to create an array by pre-setting its length and an optional filling value that is both straightforward and safe.\
We pass from cryptic and verbous solutions like `Array.apply(null, Array(N)).map(mapperFn)`, to only verbous solution like `Array.from({length:N}, mapperFn)` and `[...Array(N)].map(mapperFn)` or a dangerous one like `Array(N).fill(value)`.

Why is the last dangerous? Spot the bug here:
```js
const grid = Array(2).fill(Array(2).fill(false));

grid[0][0] = true;
grid[0][1] = true;

console.log(String(grid));
// why is true, true, true, true
// instead of true, true, false, false?
```

## solution
The new static method was developed with safety and simplicity in mind, without straying too far from the dynamic nature of the language.

Calling `Array.make` without arguments is allowed and an empty array is returned:
```js
Array.make(); // []
```
Remember that `undefined` means the absence of value in JavaScript:
```js
Array.make(undefined); // []
```

If provided, the first argument should be a `number` or a `string` coercible into a valid numeric value. There are some restriction about the numeric values to be considered valid: the value should be different from `NaN`, positive and finite:
```js
Array.make(NaN); // TypeError
Array.make(Infinite); // TypeError
Array.make(-1); // TypeError
Array.make(null); // TypeError
Array.make({}); // TypeError
Array.make([]); // TypeError
Array.make(() => {}); // TypeError
Array.make(Symbol()); // TypeError
Array.make("foo"); // TypeError

Array.make(0); // Ok
Array.make(10); // Ok
Array.make("42"); // Ok
```

The default value used to fill the array is `undefined`. Forget about `empty slots`:
```js
Array.make(5); // [undefined, undefined, undefined, undefined, undefined]
```

The second argument is used to set the filling value/values:
```js
Array.make(5, "foo"); // ["foo", "foo", "foo", "foo", "foo"]
```

To avoid the `Array.prototype.fill` dangerousness, you cannot pass an object nor an array as second argument:
```js
Array.make(5, {}); // TypeError
Array.make(5, []); // TypeError
```
That will prevent creating an array with more than one reference to the same entity.\
This is an unwelcome behavior intrinsically linked to the `fill` method; something that should be clearly reported with an exception instead of being allowed, causing hard to catch bugs.

When you need to fill the array with non primitive entities, pass a function as second argument. It will act both as a factory function and a mapper function:
```js
Array.make(3, (_, index, arrayUnderCostruction) => ({ safe: true, id: index }));
// [{ safe: true, id: 0 }, { safe: true, id: 1 }, { safe: true, id: 2 }]
```

## pitfalls

Yes, I'm adding a static method to a native entity. If you don't like this approach, don't use my package.\
I don't think that such a method will be ever added to the language, but if it were so I'm sure that a different name will be chosen. Like `fromLength`.
TC39 i going to hate me for this..\
\
Peace ✌🏻,
174 changes: 174 additions & 0 deletions __tests__/arraymake.test.js
@@ -0,0 +1,174 @@
require("../src/index");

describe("sanity check", () => {
test('should pass', () => {
expect(true).toBe(true);
});
});

describe("Array.make", () => {

test('should return an empty array because the length argument is implicitly undefined', () => {
expect(Array.make()).toEqual([]);
});

test('should return an empty array because the length argument is explicitly undefined', () => {
expect(Array.make()).toEqual([]);
});

test('should properly detect a wrong length argument because it is null', () => {
expect(() => {
Array.make(null);
}).toThrow(TypeError);
});

test('should properly detect a wrong length argument because it is an object', () => {
expect(() => {
Array.make({});
}).toThrow(TypeError);
});

test('should properly detect a wrong length argument because it is an array', () => {
expect(() => {
Array.make([]);
}).toThrow(TypeError);
});

test('should properly detect a wrong length argument because it is a function', () => {
expect(() => {
Array.make(() => { });
}).toThrow(TypeError);
});

test('should properly detect a wrong length argument because it is a Symbol', () => {
expect(() => {
Array.make(Symbol());
}).toThrow(TypeError);
});

test('should properly detect a wrong length argument because it is NaN', () => {
expect(() => {
Array.make(NaN);
}).toThrow(TypeError);
});

test('should properly detect a wrong length argument because it is Infinity', () => {
expect(() => {
Array.make(Infinity);
}).toThrow(TypeError);
});

test('should properly detect a wrong length argument because it is less than 0', () => {
expect(() => {
Array.make(-1);
}).toThrow(TypeError);
});

test('should properly detect a wrong length argument because it is a string not convertible to a numerical value', () => {
expect(() => {
Array.make("q");
}).toThrow(TypeError);
});

test('should return an empty array', () => {
expect(Array.make(0)).toEqual([]);
});

test('should return an array with 5 undefined elements', () => {
expect(Array.make(5)).toEqual([...Array(5)]);
});

test('should accept a string value for the length property if it is convertible into a valid number', () => {
expect(Array.make("5")).toEqual([...Array(5)]);
});

test('should properly detect a wrong second argument because it is an object', () => {
expect(() => {
Array.make(1, {});
}).toThrow(TypeError);
});

test('should properly detect a wrong second argument because it is an array', () => {
expect(() => {
Array.make(1, []);
}).toThrow(TypeError);
});

test('should return an array with 5 null elements', () => {
expect(Array.make(5, null)).toEqual([null, null, null, null, null]);
});

test('should return an array with 5 number 42 elements', () => {
expect(Array.make(5, 42)).toEqual([42, 42, 42, 42, 42]);
});

test('should return an array with 5 string elements', () => {
expect(Array.make(5, "hi")).toEqual(["hi", "hi", "hi", "hi", "hi"]);
});

test('should return an array with 5 Symbol elements', () => {
const s = Symbol();
expect(Array.make(5, s)).toEqual([s, s, s, s, s]);
});

test('should return an array with 5 true elements', () => {
expect(Array.make(5, true)).toEqual([true, true, true, true, true]);
});

test('should return an array with 5 false elements', () => {
const s = Symbol();
expect(Array.make(5, false)).toEqual([false, false, false, false, false]);
});

test('should call the mapperFn 5 times', () => {
const mockFn = jest.fn();
Array.make(5, mockFn);
expect(mockFn).toHaveBeenCalledTimes(5);
});

test('the mapperFn should act like a factory', () => {
const value = {};
const factory = () => value;
const res = Array.make(5, factory);
expect(res).toEqual([value, value, value, value, value]);
});

test('each fresh non-primitive value returned by a factory must be unique', () => {
const factory = () => ({});
const res = Array.make(5, factory);
expect(res.indexOf(res[0])).toBe(0);
expect(res.indexOf(res[1])).toBe(1);
expect(res.indexOf(res[2])).toBe(2);
expect(res.indexOf(res[3])).toBe(3);
expect(res.indexOf(res[4])).toBe(4);
});

test('the mapperFn should receive always undefined as first argument', () => {
let isAlwaysUndefined = true;
const factory = first => {
if (first !== void 0) isAlwaysUndefined = false;
};
Array.make(5, factory);
expect(isAlwaysUndefined).toBe(true);
});

test('the mapperFn should receive the current index as second argument', () => {
let indexes = [];
const factory = (_, i) => {
indexes.push(i);
};
Array.make(5, factory);
expect(indexes).toEqual([0, 1, 2, 3, 4]);
});

test('the mapperFn should receive the array in construction as third argument', () => {
let receivedArray;
const factory = (_, i, array) => {
receivedArray = array;
};
const res = Array.make(1, factory);
expect(res).toEqual(receivedArray);
});


});
13 changes: 13 additions & 0 deletions babel.config.js
@@ -0,0 +1,13 @@
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
};

0 comments on commit 22caa7a

Please sign in to comment.