art-standard-lib
Version:
The Standard Library for JavaScript that aught to be.
500 lines (417 loc) • 13.1 kB
text/coffeescript
{bound, max, intRand, modulo} = require "./MathExtensions"
{isFunction, isNumber, isString} = require './TypesExtended'
{wordsRegex, exactlyOneWordRegex} = require './RegExpExtensions'
module.exports = class ArrayExtensions
###
Useful compact and compactFlatten keepTester functions
###
@keepAll: keepAll = -> true
@keepIfRubyTrue: keepIfRubyTrue = (a) -> a != undefined && a != null && a != false
# OUT: the array
@reverseForEach: (array, f) ->
f v for v in array by -1
array
@arrayToTruthMap: (array) ->
res = {}
for a in array
res[a] = true
res
@arrayToFalseMap: (array) ->
res = {}
for a in array
res[a] = false
res
@arrayAfterEach: (array, value) ->
out = []
for v in array
out.push v
out.push value
out
@arrayBeforeEach: (array, value) ->
out = []
for v in array
out.push value
out.push v
out
@arrayBetweenEach: (array, value) ->
out = []
for v, i in array
out.push value if i > 0
out.push v
out
@concatInto: (array, b) -> array.push.apply array, b
@uniqueValues: (sortedArray, eqF = ((a, b) -> a == b)) ->
v for v, i in sortedArray when i == 0 || !eqF v, sortedArray[i-1]
###
IN:
array: an array or falsy value
element: anything
OUT:
array containing element as the last element
EFFECT:
if array was falsy, a new length-1 array is returned
else, array was mutated by pushing the current element
WHY?
Why write this when arrays alread have push?
1) if array is null, this works as desired
2) this returns array, not array.length
Returning the array is what Ruby's push does.
It makes chaining pushes easy.
###
@push: (array, element) ->
if array
array.push element
array
else
[element]
@peek: (array, offset = -1) =>
if array?
array[array.length + offset]
else
undefined
basicCompareFunction = (a, b) -> a - b
# if index < 0, index += array.length
@leftOfIndex: leftOfIndex = (array, index) ->
return array unless array
array.slice 0, index
# if index < 0, index += array.length
@rightOfIndex: rightOfIndex = (array, index) ->
return array unless array
if index < 0
index += array.length
array.slice index + 1
# if value is in array, return the index of the first match
# else, return the length of the array
indexOfOrLength = (array, value) ->
if 0 > i = array.indexOf value
array.length
else
i
# if value is not found, left returns whole array, right returns nothing
# if value is found, left returns all elements before the first match, right returns all elements after
@leftOf: (array, value) -> leftOfIndex array, indexOfOrLength array, value
@rightOf: (array, value) -> rightOfIndex array, indexOfOrLength array, value
@splitArray: (array, value) ->
index = indexOfOrLength array, value
[
leftOfIndex array, index
rightOfIndex array, index
]
###
findSortedFirst
Acts as-if it sorted the array and returned the first element.
Details:
tests each element in the array againts the current "smallest"
returns the element for which tests "smaller" than every other
element a is "smaller" than b if compareFunction(a, b) returns >0 value
IN:
array - the array to search or null
compareFunction - (a, b) -> # standard compare function
returns:
0: if a and b are equal
<0: if b is greater than a
>0: if a is greater than b
default: (a, b) -> a - b
OUT:
largest value in array or undefined if array is null or length 0
###
@findSortedFirst: (array, compareFunction = basicCompareFunction) ->
return undefined unless array?.length > 0
returnElement = array[0]
for i in [1...array.length] by 1
if 0 < compareFunction returnElement, element = array[i]
returnElement = element
returnElement
@first: (array) => array[0]
@second: (array) => array[1]
@third: (array) => array[2]
@forth: (array) => array[3]
@fifth: (array) => array[4]
@last: (array) => if array then array[array.length - 1] else undefined
# push item into array unless it is already present in the array
# returns true if the item was pushed
@pushIfNotPresent: (array, item) =>
if item in array
false
else
array.push item
true
# ALIAS
@pushUnique: @pushIfNotPresent
###
IN:
array: []
randomizer: [optional] () -> [0, 1) random number generator
selectFromFirstNElements: [optional] int
###
@randomElement: randomElement = (array, a, b) ->
rand = if isFunction randomizer = a
fromFirstN = b
randomizer()
else
fromFirstN = a
Math.random()
array[rand * (fromFirstN ? array.length) | 0]
@randomSortInPlace: randomSortInPlace = (array) ->
len = array.length
for i in [len-1..0] by -1
j = intRand i
a = array[i]
array[i] = array[j]
array[j] = a
array
# new array, randomly sorted
# null/undefined > []
@arrayWithRandomSort: (array) ->
if array
randomSortInPlace array.slice()
else
[]
@randomSort: @arrayWithRandomSort
# insert -1 => add to end of array
# insert -2 => insert one before the last element of the array
@insert: (array, index, item) ->
index = array.length + index + 1 if index<0
array.splice index, 0, item
array
# same as insert, except doesn't modify input array; returns new array
@arrayWithInsertedValue: arrayWithInsertedValue = (array, index, item) =>
@insert array.slice(), index, item
@withInserted: arrayWithInsertedValue
# returns a new, sorted array
@withSort: (array = [], sortFunction) ->
array = array.slice()
array.sort sortFunction
@remove: (array, index, amount = 1) ->
index = array.length + index + 1 if index<0
array.splice index, amount
array
@removeFirstMatch: (array, toMatchValue) =>
index = array.indexOf toMatchValue
if index >= 0
@remove array, index
else
array
@arrayWithout: (array, index, amount = 1) =>
if array?
index = array.length - 1 unless index?
@remove array.slice(), index, amount
else []
# OUT: returns the exact same array if value isn't present
# else if array is null/undefined, returns []
# else, returns the array with the first occurance of value removed.
@arrayWithoutValue: (array, value) =>
if array?
if 0 <= index = array.indexOf value
@remove array.slice(), index, 1
else
array
else []
@arrayWithoutLast: (array, amount = 1) ->
if array? && amount < array.length
array.slice 0, array.length - amount
else
[]
# returns a clone of array
# pure-functional push
# with value pushed at the end (value can be duplicated)
# if array is null/undefined/false, returns new array with only value in it: [value]
@arrayWith: (array, value) =>
return [value] unless array
array = array.slice()
array.push value
array
@truncatedArrayWith: (array, length, value) =>
return [value] unless array
array = array.slice 0, length
array.push value
array
# returns a clone of array without the last element
# pure-functional pop
@poppedArray: (array) =>
array.slice 0, array.length - 1
# returns a clone of array (or [value] if array is null)
# with value pushed at the end ONLY IF value isn't already in the array
# if array is null/undefined/false, returns new array with only value in it: [value]
@arrayWithOne: (array, value) =>
return [value] unless array
array = array.slice()
array.push value unless value in array
array
# ALIAS
@arrayWithUnique: @arrayWithOne
@slice: (a, b, c) -> arraySlice.call a, b, c
@shuffleArray: (a) ->
i = a.length
while --i > 0
j = ~~(Math.random() * (i + 1))
t = a[j]
a[j] = a[i]
a[i] = t
a
# included here only for testing; use @moveArrayElement as a client
@_moveArrayElementLargeArray: _moveArrayElementLargeArray = (array, from, to) ->
array.splice to, 0, array.splice(from, 1)[0]
array
@_moveArrayElementSmallArray: _moveArrayElementSmallArray = (array, from, to) ->
from = from | 0
to = to | 0
tmp = array[from]
if from < to then array[i] = array[i + 1] for i in [from..to-1] by 1
else array[i] = array[i - 1] for i in [from..to+1] by -1
array[to] = tmp
array
# move one element of the array FROM one location TO another.
# IN: from/to are moduloed into valid indexes
# a = intMode a, array.length
@moveArrayElement: moveArrayElement = (array, from, to) ->
from = modulo from | 0, array.length
to = modulo to | 0, array.length
# 300 is a somewhat arbitrary value. It's approximately where
# _moveArrayElementLargeArray becomes faster on Chrome 36.0.1985.143 (2014/9/21)
# http://jsperf.com/array-prototype-move/19
if Math.abs(from - to) > 300
_moveArrayElementLargeArray array, from, to
else
_moveArrayElementSmallArray array, from, to
array
@arrayWithElementMoved: arrayWithElementMoved = (array, from, to) ->
from = modulo from | 0, array.length
to = modulo to | 0, array.length
return array if from == to
array = array.slice()
moveArrayElement array, from, to
@arrayWithElementValueMoved: (array, value, to) ->
from = array.indexOf value
return array if from < 0
arrayWithElementMoved array, from, to
@arrayWithElementReplaced: (array, value, index) ->
array = array.slice()
array[index] = value
array
@arrayWithAllButLast: (array, amount = 1) ->
if array
array.slice 0, array.length - amount
else
[]
###
stableSort is an in-place, stable sort
"stable" means that if two elements are 'equal' under the compare test, their order won't
change with respect to each other.
NOTE: array.sort is not guaranteed to be stable
###
@stableSort: (array, compare) ->
compare ||= (a, b) -> a - b
# FIRST PASS - a BUBBLE SORT
notSorted = true
length = array.length
while notSorted
notSorted = false
for i in [1...length] by 1
if compare(a = array[i-1], b = array[i]) > 0
array[i-1] = b
array[i] = a
notSorted = true
array
# compares two arrays, element by element using == (javascript ===)
# returns the longest array of elements which are:
# * in both a and b, and
# * in the same order in both a and b
# FUTURE: should we use shallowEq?
@longestCommonSubsequence = longestCommonSubsequence = (a, b) ->
lcs = []
row = []
c = []
# make sure shorter string is the column string
if m < n
s = a
a = b
b = s
m = a.length
n = b.length
# build the c-table
row[j] = 0 for j in [0...n] by 1
for i in [0...m] by 1
c[i] = row = row.slice()
diag = 0
for j in [0..n-1] by 1
latch = row[j]
if a[i] == b[j]
row[j] = diag + 1
else
left = row[j - 1] || 0
if left > row[j]
row[j] = left
diag = latch
i--
j--
# row[j] now contains the length of the lcs
# recover the lcs from the table
while i > -1 && j > -1
switch c[i][j]
when i && c[i-1][j]
i--
continue
when j && c[i][j-1]
j--
else
j--
lcs.unshift a[i]
i--
continue
lcs
# goal 1: Given a and b, merge them while maintain order.
# goal 2: Given a and b contain no duplicates, returns an array with no duplicates.
# As the simple example below shows, goal 1 and to can come into conflict. If so, b's order
# is preferred and a's order is compromized.
# Ex: a = [1, 2], b = [2, 1]
# goal-1-only-result: [2, 1, 2]
# goal-2 with b-order dominance: [2, 1]
@minimumOrderedOverlappingMerge = (a, b) ->
c = longestCommonSubsequence a, b
m = a.length
n = b.length
o = c.length
out = new Array n
i = 0
j = 0
k = 0
l = 0
while i < m && j < n && k < o
ai = a[i]
bj = b[j]
ck = c[k]
if ai == ck && bj == ck
i++
j++
k++
out[l++] = ai
else if ai != ck
i++
unless ai in b # skip duplicates
out[l++] = ai
else
j++
out[l++] = bj
while i < m
ai = a[i++]
out[l++] = ai unless ai in b
while j < n
out[l++] = b[j++]
out
# SSNNSOONSSOONSON
# SSSOOSSOOSO
# SSNNSNSSNSN
# SSSSSS
# trying out various aliases to see which one I like
@wordsArray: w = (args...)->
out = []
for arg in args
if isString(arg) && !arg.match exactlyOneWordRegex
out = out.concat arg.match wordsRegex
else
out.push arg
out
@wordArray: @wordsArray
@w: @wordsArray # this one makes it feel a lot like ruby's %w{a b c}. Instead, we have: w "a b c"
@a: a = (args...)-> args