Skip to content

Instantly share code, notes, and snippets.

@MattMcFarland
Created April 14, 2017 03:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MattMcFarland/d96d3133eb59ff248129f67a6eb4c39e to your computer and use it in GitHub Desktop.
Save MattMcFarland/d96d3133eb59ff248129f67a6eb4c39e to your computer and use it in GitHub Desktop.
A 2d grid library (with unit tests)
const assert = require('assert')
const transpose = (array) => array.reduce((p, n) => n.map((item, i) => [...(p[i] || []), n[i]]), [])
/**
* @class Grid
*/
class Grid {
/**
* @static transpose
* @param grid - grid to transpose
* @returns new grid that is transposed.
*/
static transpose (grid) {
let _grid = new Grid(grid.height, grid.width, undefined)
_grid.cells = transpose(grid.cells)
return _grid
}
/**
* @static fromArray
* @param array {Array[][]} 2D array[x][y]
* @returns new grid
*/
static fromArray (arr) {
assert(arr.length, 'array must be 2D eg [][]')
assert(arr[0].length, 'array must be 2D eg [][]')
const width = arr.length
const height = arr[0].length
return new Grid(width, height, (x, y) => arr[x][y])
}
/**
* @constructor
* @param width {Integer} x-axis size
* @param height {Integer} y-axis size
* @param fill {any|Function} set the value for every sell, if you pass a function it will pass x,y and accept its return value
*/
constructor (width, height, fill) {
assert(Number.isInteger(width), 'argument must be an integer')
assert(Number.isInteger(height), 'argument must be an integer')
this.cells = []
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
this.cells[x] = this.cells[x] || []
this.cells[x][y] = typeof fill === 'function' ? fill(x, y) : fill
}
}
}
/**
* @property {Integer} length
*/
get length () {
return this.width * this.height
}
/**
* @property {Integer} length
*/
get width () {
return this.cells.length
}
/**
* @property {Integer} height
*/
get height () {
return this.cells[0].length
}
/**
* @method setCell
* @param x {Integer}
* @param y {Integer}
* @returns {any} value
*/
setCell (x, y, value) {
this.cells[x][y] = value
return this
}
mutate (fromX, fromY, toX, toY, cb) {
assert(Number.isInteger(fromX), 'argument must be an integer')
assert(Number.isInteger(fromY), 'argument must be an integer')
assert(Number.isInteger(toX), 'argument must be an integer')
assert(Number.isInteger(toY), 'argument must be an integer')
const x1 = Math.min(fromX, toX)
const y1 = Math.min(fromY, toY)
const x2 = Math.max(fromX, toX)
const y2 = Math.max(fromY, toY)
for (let y = y1; y <= y2; y++) {
for (let x = x1; x <= x2; x++) {
this.setCell(x, y, cb(this.cells[x][y], x, y))
}
}
return this
}
/**
* @param cb {Function} callback function
*/
forEach (cb) {
for (let x = 0; x <= this.width - 1; x++) {
for (let y = 0; y <= this.height - 1; y++) {
cb(this.cells[x][y], x, y)
}
}
return this
}
/**
* @returns a new array representing the 2d array from grid.
*/
toArray () {
return [...this.cells]
}
toString (xDelimeter = ',', yDelimeter = '|') {
let str = ''
this.forEach((cell, x, y) => {
str += cell
if (x < this.width && y < this.height - 1) str += xDelimeter
if (y === this.height - 1 && x < this.width - 1) str += yDelimeter
})
return str
}
map (cb) {
let _grid = new Grid(this.width, this.height, undefined)
this.forEach((cells, x, y) => {
_grid.cells[x][y] = cb(cells, x, y)
})
return _grid
}
}
module.exports = Grid
const Grid = require('../lib/Grid')
const test = require('tape')
test('Grid', (t) => {
t.test('props', (t) => {
const grid = new Grid(3, 3, 0)
t.assert(grid.length === 9, 'length')
t.assert(grid.width === 3, 'width')
t.assert(grid.height === 3, 'height')
t.end()
})
t.test('forEach', (t) => {
const grid = new Grid(3, 3, 0)
grid.forEach((value, x, y) => {
t.assert(value === 0, `grid at ${x},${y} is zero`)
})
t.end()
})
t.test('map', (t) => {
const grid = new Grid(3, 3, 0)
const grid2 = grid.map(value => 1)
grid.forEach((value, x, y) => {
t.assert(value === 0, `original grid at ${x},${y} is still 0`)
})
grid2.forEach((value, x, y) => {
t.assert(value === 1, `mapped grid at ${x},${y} is 1`)
})
t.end()
})
t.test('toString', (t) => {
const grid = new Grid(3, 3, 0).map((v, x, y) => x)
t.assert(grid.toString() === '0,0,0|1,1,1|2,2,2', 'default delimeter')
t.assert(grid.toString('-') === '0-0-0|1-1-1|2-2-2', 'x delimeter')
t.assert(grid.toString('-', '*') === '0-0-0*1-1-1*2-2-2', 'y delimeter')
t.end()
})
t.test('toArray', (t) => {
const grid = new Grid(2, 3, 0).map((v, x, y) => y)
grid.toArray().forEach((row, x) => {
row.forEach((value, y) => {
t.assert(grid.cells[x][y] === value)
})
})
t.end()
})
t.test('fromArray', (t) => {
const grid = new Grid(3, 2, 0).map((v, x, y) => y)
const gridA = grid.toArray()
const grid2 = Grid.fromArray(gridA)
t.assert(grid2.toString() === grid.toString())
t.end()
})
t.test('mutate', (t) => {
const grid = Grid.fromArray([[0, 0, 0], [1, 1, 1], [2, 2, 2]])
grid.mutate(0, 0, 2, 0, (cell) => 'x')
t.assert(grid.toString() === 'x,0,0|x,1,1|x,2,2', 'change first row')
grid.mutate(2, 0, 0, 0, (cell) => 'z')
t.assert(grid.toString() === 'z,0,0|z,1,1|z,2,2', 'iterates in reverse')
t.end()
})
t.test('transpose', (t) => {
const _grid = new Grid(2, 4, 0).map((v, x, y) => x + y)
const grid = Grid.transpose(_grid)
t.assert(grid.width === _grid.height, 'height reflects transposition')
t.assert(grid.height === _grid.width, 'width reflects transposition')
t.assert(grid.cells[0][1] === _grid.cells[1][0], 'values transposed')
t.end()
})
t.end()
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment