I am trying to implement the game 2048 using JavaScript. I am using a two-dimensional array to represent the board. For each row, it is represented using an array of integers.
Here I am focused on implementing the merge left functionality i.e. the merge that happens after the user hits left on their keyboard.
Here are a set of test cases that I came up with
const array1 = [2, 2, 2, 0] // [4,2,0,0]
const array2 = [2, 2, 2, 2] // [4,4,0,0]
const array3 = [2, 0, 0, 2] // [4,0,0,0]
const array4 = [2, 2, 4, 16] // [4,4,16,0]
The commented part is the expected results after merge left happened.
Here is my attempt
const arrays = [
[2, 2, 2, 0], // [4,2,0,0]
[2, 2, 2, 2], // [4,4,0,0]
[2, 0, 0, 2], // [4,0,0,0]
[2, 2, 4, 16] // [4,4,16,0]
];
function mergeLeft(array) {
let startIndex = 0
let endIndex = 1
while (endIndex < array.length) {
if (array[startIndex] === array[endIndex]) {
array[startIndex] = array[startIndex] + array[endIndex]
array[endIndex] = 0
startIndex++
}
endIndex++
}
return shift(array, 'left')
}
function shift(array, dir) {
if (dir === 'left') {
for (let i = 0; i < array.length - 1; i++) {
if (array[i] === 0) {
[array[i], array[i + 1]] = [array[i + 1], array[i]]
}
}
}
// omitting when dir === 'right', 'up', 'down' etc.
return array
}
arrays.forEach(a => console.log(mergeLeft(a)));
So the idea here is that I merged the array and then shift the non-zero items to the left.
My current solution is buggy for this particular case - when the array is [2, 2, 2, 2], the output is [4,2,2,0] when the expected output is [4,4,0,0]
I know that my implementation is not elegant either. So I would love to see how this can be implemented in a (much) better way.
By the way I found on code review stack exchange there is a python implementation that seems to be working. However, I don't really know Python nor functional programming paradigm. I would appreciate it if someone can take a look at it and see if how this can be translated into JavaScript
I think a recursive version is simplest here:
const zeroFill = xs =>
xs .concat ([0, 0, 0, 0]) .slice (0, 4)
const shift = ([n0, n1, ...ns]) =>
n0 == undefined
? []
: n0 == 0
? shift ([n1, ...ns])
: n1 == 0
? shift ([n0, ...ns])
: n0 == n1
? [n0 + n1, ... shift (ns)]
: [n0, ...shift ([n1, ... ns])]
const shiftLeft = (ns) =>
zeroFill (shift (ns))
const arrays = [[2, 2, 2, 0], [2, 2, 2, 2], [2, 0, 0, 2], [2, 2, 4, 16], [0, 8, 2, 2], [0, 0, 0, 0]];
arrays .forEach (
a => console.log(`${JSON .stringify (a)}: ${JSON .stringify (shiftLeft (a))}`)
)
Our basic shift is wrapped with zeroFill, which adds trailing zeros to the the array, to make it four long.
The main function is shift, which does a shift-left of a row, but if I were to build a complete 2048, I would used this for all shifts, simply translating the directions to the indices required. It works like this:
If our array is empty, we return an empty array
If the first value is zero, we ignore it and continue with the rest of the array
If the second value is zero, we remove it and recur with the remainder (including the first value)
If the first two values are equal, we combine them for the first spot and recur on the remainder
Otherwise, we keep the first value, and then recur on everything else (including the second value)
Although we could remove the wrapper, merging the zero-filling into the main function, so that, for instance in the second case, instead of returning shift([n1, ...ns]) we would return zeroFill(shift([n1, ...ns])). But that would mean calling the zero-fill several times for no good reason.
Update
A comment asked for clarification on how I would use this for shifting boards in all directions. Here is my first thought:
// utility functions
const reverse = (xs) =>
[...xs] .reverse();
const transpose = (xs) =>
xs [0] .map ((_, i) => xs .map (r => r[i]))
const rotateClockwise = (xs) =>
transpose (reverse (xs))
const rotateCounter = (xs) =>
reverse (transpose (xs))
// helper functions
const shift = ([n0, n1, ...ns]) =>
n0 == undefined
? []
: n0 == 0
? shift ([n1, ...ns])
: n1 == 0
? shift ([n0, ...ns])
: n0 == n1
? [n0 + n1, ... shift (ns)]
: [n0, ... shift ([n1, ... ns])]
const shiftRow = (ns) =>
shift (ns) .concat ([0, 0, 0, 0]) .slice (0, 4)
// main functions
const shiftLeft = (xs) =>
xs .map (shiftRow)
const shiftRight = (xs) =>
xs .map (x => reverse (shiftRow (reverse (x))))
const shiftUp = (xs) =>
rotateClockwise (shiftLeft (rotateCounter (board)))
const shiftDown = (xs) =>
rotateClockwise (shiftRight (rotateCounter (board)))
// sample data
const board = [[4, 0, 2, 0], [8, 0, 8, 8], [2, 2, 4, 8], [0, 0, 4, 4]]
// demo
const display = (title, xss) => console .log (`----------------------\n${title}\n----------------------\n${xss .map (xs => xs .map (x => String(x).padStart (2, ' ')) .join(' ')).join('\n')}`)
display ('original', board)
display ('original shifted left', shiftLeft (board))
display ('original shifted right', shiftRight (board))
display ('original shifted up', shiftUp (board))
display ('original shifted down', shiftDown (board))
.as-console-wrapper {max-height: 100% !important; top: 0}
We start with function to reverse a copy of an array, and to transpose a grid over the main diagonal (northwest to southeast). We combine those two in order to create functions to rotate a grid clockwise and counter-clockwise. Then we include the function discussed above, slightly renamed, and with the zero-fill helper inlined.
Using these we can now write our directional shift function fairly easily. shiftLeft just maps shiftRow over the rows. shiftRight first reverses the rows, calls shiftLeft and then reverses them again. shiftUp and shiftDown rotate the board counter-clockwise call shiftLeft and shiftRight, respectively, and then rotates the board clockwise.
Note that none of these main functions mutate your data. Each returns a new board. That is one of the most important tenets of functional programming: treat data as immutable.
This is not a full 2048 system. It doesn't randomly add new 2s or 4s to the board, nor does it have any notion of a user interface. But I think it's probably a reasonably solid core for a functional version of the game..
You can try this.
const arrays = [
[2, 2, 2, 0], // [4,2,0,0]
[2, 2, 2, 2], // [4,4,0,0]
[2, 0, 0, 2], // [4,0,0,0]
[2, 2, 4, 16] // [4,4,16,0]
];
function shiftLeft(array) {
op = []
while(array.length!=0){
let v1 = array.shift();
while(v1==0 && array.length>0){
v1 = array.shift();
}
if(array.length==0){
op.push(v1);
}else{
let v2 = array.shift();
while(v2==0 && array.length>0){
v2 = array.shift();
}
if(v1==v2){
op.push(v1+v2);
}else{
op.push(v1);
array.unshift(v2);
}
}
}
while(op.length!=4){
op.push(0);
}
return op
}
arrays.forEach(a => console.log(shiftLeft(a)));
Here is a function that performs the merge and shift in one loop:
function mergeLeft(array) {
let startIndex = 0;
for (let endIndex = 1; endIndex < array.length; endIndex++) {
if (!array[endIndex]) continue;
let target = array[startIndex];
if (!target || target === array[endIndex]) { // shift or merge
array[startIndex] += array[endIndex];
array[endIndex] = 0;
} else if (startIndex + 1 < endIndex) {
endIndex--; // undo the next for-loop increment
}
startIndex += !!target;
}
return array;
}
// Your tests:
const arrays = [
[2, 2, 2, 0], // [4,2,0,0]
[2, 2, 2, 2], // [4,4,0,0]
[2, 0, 0, 2], // [4,0,0,0]
[2, 2, 4, 16] // [4,4,16,0]
];
for (let array of arrays) console.log(...mergeLeft(array));
Explanations
The for loop increments the endIndex from 1 to 3 included. This index represents a potential value that needs to shift and/or merge.
If that index refers to an empty slot (value is 0), then nothing needs to happen with it, and so we continue with the next iteration of the loop.
So now we are in the case where endIndex refers to a non-zero value. There are two cases where something needs to happen with that value:
The value at startIndex is zero: in that case the value at endIndex must move to startIndex
The value at startIndex is equal to that at endIndex: in that case the value at endIndex must also move to startIndex, but adding to it what was already there.
These cases are very similar. In the first case we could even say that the value at endIndex is added to the one at startIndex since the latter is zero. So these two cases are handled in one if block.
If we are not in either of these two cases then we know that the value at startIndex is non-zero and different from the one at endIndex. In that case we should leave the value at startIndex unaltered and just move on. However, we should reconsider the value of this same endIndex again in the next iteration, as it might need to move still. So that is why we do endIndex-- so to neutralise the loop's endIndex++ that will happen one instant later.
There is one case where we do want to go to the next endIndex: that is when startIndex would become equal to endIndex: that should never be allowed in this algorithm.
Finally, startIndex is incremented when it originally had a non-zero value. However, if it was zero at the start of this iteration, it should be reconsidered in the next iteration of the loop. So then we do not add 1 to it. startIndex += !!target is just another way for doing:
if (target > 0) startIndex++;
Related
How to generate an array of incremental values from a given array
the idea is to create a kind of diamond shape where the arrays start decreasing in size once they reach the middle of the array. In other words the longest array is going to be the one that is containing the middle value of the array or (array.length/2 + 1)
and in cases where the elements are short to complete the array on the second half just replace it with 'E' to indicate empty space just like on the second example.
example 1
var array = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p']
//the longest array in length is containing 'i' which is the value at
array.length/2 + 1
var output = [
['a'],
['b','c'],
['d','e','f'],
['g','h','i','j'],
['k','l','m'],
['n','o'],
['p']
]
example 2
var array = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t']
enter code here
//the longest array in length is containing 'k' which is the value at array.length/2 + 1
var output = [
['a'],
['b','c'],
['d','e','f'],
['g','h','i','j'],
['k','l','m','n','o'],
['p','q','r','s'],
['t','E','E'],
['E','E'],
['E]
]
Here is the code i have tried:
const values = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16];
const halfLen = values.length/2 + 1;
var topArr = [];
for(let i = 0; i < values.length; i ++) {
if(i <= halfLen) {
topArr.push(values[i])
}
}
console.log(topArr)
var filTopArr = [];
for(let i = 0; i <= topArr.length; i ++) {
let prevIndex = i - 1;
if(i === 0) {
filTopArr.push(topArr[i])
} else if(i === 1) {
filTopArr.push(topArr.slice(i, i + i + 1))
} else {
filTopArr.push(topArr.slice(i, i + i ))
}
}
console.log(filTopArr)
my idea here was to separate the array into two different arrays which are going to be the top part that is incrementing in size and the second/bottom part that is going to be decreasing in size.
The above code had this output
[1, [2, 3], [3, 4], [4, 5, 6], [5, 6, 7, 8], [6, 7, 8, 9], [7, 8, 9], [8, 9], [9], []]
Some observations:
The number of strings in the output (including the padding "E" strings) is always a perfect square (1, 4, 9, 16, 25, ...etc)
In order to know how many "E" strings need to be added, we thus need to know which is the least perfect square that is not less than the input size.
The longest (middle) subarray in the output has a size that is the square root of that perfect square.
The number of subarrays is the double of that number minus 1.
This leads to the following implementation:
function diamond(array) {
// Get least perfect square that is not less than the array length
const sqrt = Math.ceil(Math.sqrt(array.length));
const size = sqrt ** 2;
// Pad the array with "E" strings so to reach that perfect square size
const all = [...array, ..."E".repeat(size - array.length)];
const length = 2 * sqrt;
return Array.from({length}, (_, width) => {
return all.splice(0, Math.min(width, length - width));
}).slice(1); // Skip the first subarray that was produced (empty array)
}
// Demo using the two provided examples:
var array = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p'];
console.log(diamond(array));
var array = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t'];
console.log(diamond(array));
Here's a recursive version. Note that display is just for presentation purposes.
The only real work is in diamond:
const diamond = (xs, [len = xs.length, up = true, n = 1] = []) => n == 0 ? [] : [
Object .assign (Array (n) .fill ('E'), xs .slice (0, n)),
...diamond (xs .slice (n), up && n * n < len ? [len, true, n + 1] : [len, false, n - 1])
]
const display = (xss) => console .log (`${xss .map (
(xs, i) => `${' '.repeat (Math .abs ((xss .length - 1) / 2 - i) + 1)}${xs .join (' ')
}`) .join ('\n')}`)
const demos = [
['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p'],
['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u'],
[1, 2, 3, 4, 5, 6, 7, 8]
]
demos .forEach (array => display (diamond (array)))
.as-console-wrapper {max-height: 100% !important; top: 0}
We track the length of the current string (n, defaulting to 1), the length of the original array (len) , and a boolean flag to tell whether our length is moving up or down (up). We increase n on initial iterations, adding the next n characters from our input as the next subarray. When n hits zero we return an empty array. When n ** n is greater than or equal to len, we switch up to false and start subtracting one from n from then on. The only other necessity is to fill our remaining array with 'E's. We do this with an Object .assign call.
If you want formatted output more like an array literal form, you could use this version of display:
const display = (xss) => console .log (`[\n${xss .map (
(xs, i) => `${' '.repeat (Math .abs ((xss .length - 1) / 2 - i) + 1)}['${xs .join (`','`)
}']`) .join ('\n')}\n]`)
to get output like this:
[
['a']
['b','c']
['d','e','f']
['g','h','i','j']
['k','l','m','n','o']
['p','q','r','s']
['t','u','E']
['E','E']
['E']
]
Note though that this recursion is a little overbearing, with three separate defaulted recursive variables. I would be as likely to go with trincot's solution as this. But it's good to have alternatives.
I've got a problema running now that is: I need a function that find the highest number made by consecutive digits within that number that I received by the parameter.
For example: If my input is 1235789 my output should be 789. If my input is 123689, my output should be 123.
function getbiggestNumber(numberInput) {
const numberString = numberInput.toString(); // turned into string
const temporaryResult = []; // create the array of possible solutions which i'd go through to find the highest value inside of it
for (let i = 0; i < numberString.length; i += 1) {
const temporary = [numberString[i]]; // create a temporary answer that would serve as a base
for (let x = i + 1; x < numberString.length; x += 1) {
const subResult = Number(numberString[i]) - Number(numberString[x]); // the result of the current number minus the following number
if (subResult === -1) { // if they are in a sequence this should be -1
temporary.push(numberString[x]); // pushing this number to that temporary answer
} // here should be some condition for it to keep running, instead getting into another number of for loop
}
temporaryResult.push(temporary); // pushing that temporary answer to the result, so I could keep track of it
}
console.log(temporaryResult); // checking the output
}
The problem is that this code is only providing double digits inside a array, and that was the only way I found to do this.
I'd be really thankful if someone could give me a light on this.
Thanks!
That looks a bit unnecessarily convoluted. I'd just split the string into chunks based on sequential digits, then call Math.max on all.
const getBiggestNumber = (numberInput) => {
const digits = [...String(numberInput)].map(Number);
const chunks = [];
let lastDigit;
let chunk = [];
for (const digit of digits) {
if (lastDigit === digit - 1) {
// Continuation of sequence
chunk.push(digit);
} else {
if (chunk.length) chunks.push(chunk);
// New sequence:
chunk = [digit];
}
lastDigit = digit;
}
chunks.push(chunk);
return Math.max(
...chunks.map(chunk => Number(chunk.join('')))
);
};
console.log(getBiggestNumber(1235789));
Another approach:
const largestStreak = (input) => Math .max (... [...String (input)] .map (Number) .reduce (
(a, d, i, xs) =>
d == a .at (-1) .at (-1) + 1
? [... a .slice (0, -1), [... a .at (-1), d]]
: [... a, [d]],
[[]]
) .map (ns => Number (ns .join (''))))
console .log (largestStreak (1235789))
console .log (largestStreak (1235689))
This goes through the following steps:
input:
1235789
[...String (input)] .map (Number):
[1, 2, 3, 4, 7, 8, 9]
reduce accumulator, step-by-step:
[[]]
[[], [1]]
[[], [1, 2]]
[[], [1, 2, 3]]
[[], [1, 2, 3], [5]]
[[], [1, 2, 3], [5], [7]]
[[], [1, 2, 3], [5], [7, 8]]
[[], [1, 2, 3], [5], [7, 8, 9]]
.map (ns => Number (ns .join (''))):
[0, 123, 5, 789]
Math .max (...):
789
Question
You are going to be given an array of integers. Your job is to take that array and find an index N where the sum of the integers to the left of N is equal to the sum of the integers to the right of N. If there is no index that would make this happen, return -1.
For example:
Let's say you are given the array {1,2,3,4,3,2,1}: Your function will return the index 3, because at the 3rd position of the array, the sum of left side of the index ({1,2,3}) and the sum of the right side of the index ({3,2,1}) both equal 6.
Answer
function findEvenIndex(arr){
for(let i = 0; i <arr.length; i++){
let arr1 = arr.slice(0, (arr[i] - 1));
let arr2 = arr.slice((arr[i] + 1),);
let arr11 = arr1.reduce((total, item)=>{
return total + item;
}, 0);
let arr22 = arr2.reduce((total, item)=>{
return total + item;
}, 0);
if(arr11 === arr22){
return arr[i];
}
}
return -1;
}
console.log(findEvenIndex([1, 2, 3, 4, 3, 2, 1]))
console.log(findEvenIndex([1, 100, 50, -51, 1, 1]))
console.log(findEvenIndex([1, 2, 3,4,5,6]))
I can't see an error here, but it yields incorrect results. Any ideas?
You have this part:
let arr1 = arr.slice(0, (arr[i] - 1));
let arr2 = arr.slice((arr[i] + 1),);
This is incorrect: arr[i]. That is a value, eg in [2,4,6,8,10] arr[3]==8. You want to slice on the index itself:
let arr1 = arr.slice(0, i - 1);
let arr2 = arr.slice(i + 1,);
Please note: There is another error in the two lines :) I leave that to you. Hint: you're now slicing two values out of the array instead of one. Perform the following code in your head first, then somewhere where you verify your results.
let arr = [0,1,2,3,4]
let x = 2;
console.log(arr.slice(0, x - 1));
console.log(arr.slice(x + 1,));
You could also use the array method findIndex, which, we shouldn't be surprised to learn, finds an index in an array subject to a certain condition.
const sum = (ns) =>
ns .reduce ((total, n) => total + n, 0)
const findBalancedIndex = (ns) =>
ns .findIndex ((_, i) => sum (ns.slice (0, i)) === sum (ns.slice (i + 1)))
console .log (findBalancedIndex ([1, 2, 3, 4, 3, 2, 1]))
console .log (findBalancedIndex ([1, 100, 50, -51, 1, 1]))
console .log (findBalancedIndex ([1, 2, 3, 4, 5, 6]))
Here we include a simple helper function to find the sum of an array, and then we pass a function to findIndex which uses it twice on the elements before the index and those after it. We use the second parameter of the callback function, the index to do this. This means we are skipping the first parameter altogether, and rather than naming it with something like n, we use the somewhat common convention of calling it _, signalling a placeholder we won't use. Note that you don't need to subtract one from the right-hand boundary of slice, since that boundary value is already excluded. And of course, others have pointed out that you need to slice to the index and not the array value at that index.
This finds the first correct index. You would have to use a different technique if you wanted to find all such indices. (That it's possible to have more than one should be clear from arrays like [1, 2, 3, 0, 0, 0, 0, 3, 2, 1] -- the indices for all those 0s would work.)
you return arr[i] when you need to return just i
In functional programming, filtering based on the characteristics of a single item is relatively straightforward -- e.g., filtering to find only odd numbers:
const arrayOfInfo = [1,2,3,4,5,6,8,10,11,13,15,16,17,19]
const onlyOddNumbers = arrayOfInfo.filter(function(item) {
return (item % 2 == 1) ? true : false
})
However, I'm not sure what the idiomatic way of doing things is if I need context -- in other words, knowing something about the surrounding items. For example, if I wanted to filter for only the items that were surrounded by odd numbers on either side, I could do this (I'm taking advantage of some JavaScript characteristics and not even bothering to check whether the indexes exist first):
const surroundedByOneOddNumber = arrayOfInfo.filter(function(item,index) {
const itemBefore = arrayOfInfo[index - 1]
const itemAfter = arrayOfInfo[index + 1]
return ((itemBefore % 2 == 1) && (itemAfter % 2 == 1)) ? true : false
})
This becomes more obvious as a problematic or inefficient way to write code if I wanted to find numbers surrounded by two odd numbers on each side:
const surroundedByTwoOddNumbers = arrayOfInfo.filter(function(item,index) {
const itemBefore = arrayOfInfo[index - 1]
const itemTwoBefore = arrayOfInfo[index - 2]
const itemAfter = arrayOfInfo[index + 1]
const itemTwoAfter = arrayOfInfo[index + 2]
return ((itemBefore % 2 == 1) && (itemTwoBefore % 2 == 1) && (itemAfter % 2 == 1) && (itemTwoAfter % 2 == 1)) ? true : false
})
Obviously, if I wanted to do something like find only numbers surrounded by 50 odd numbers on each side, this would be completely pointless to write code like this.
Is there a good way to address this with functional programming, or is this a case where it is better to drop down to for/while loop-style?
CodePen to play with the samples: https://codepen.io/jnpdx/pen/MvradM
You could use a closure over the count of the left and right needed odd values, then take the left and right values and check every element and return the result of the check for filtering.
A special case is the count of zero, there you need to check just the actual element.
var array = [1, 2, 3, 4, 5, 6, 8, 10, 11, 13, 15, 16, 17, 19],
odd = item => item % 2,
getOdds = count => (a, i, aa) => {
var temp = aa.slice(i - count, i).concat(aa.slice(i + 1, i + 1 + count));
return count
? temp.length === 2 * count && temp.every(odd)
: odd(a);
};
console.log(array.filter(getOdds(0)));
console.log(array.filter(getOdds(1)));
console.log(array.filter(getOdds(2)));
.as-console-wrapper { max-height: 100% !important; top: 0; }
A smarter approach is to count contiguous odd parts and use the array for filtering.
Check if 16 is surrounded by two odd numbers
name values comment
----- --------------------------------------------------------- --------------------
array [ 1, 2, 3, 4, 5, 6, 8, 10, 11, 13, 15, 16, 17, 19]
left [ 1, 0, 1, 0, 1, 0, 0, 0, 1, 2, 3, 0, 1, 2]
right [ 1, 0, 1, 0, 1, 0, 0, 0, 3, 2, 1, 0, 2, 1]
16 item to check
3 left count >= 2
2 right count >= 2
true result for filtering
var array = [1, 2, 3, 4, 5, 6, 8, 10, 11, 13, 15, 16, 17, 19],
odd = item => item % 2,
left = array.reduce((r, a, i) => (r[i] = odd(a) ? (r[i - 1] || 0) + 1 : 0, r), []),
right = array.reduceRight((r, a, i) => (r[i] = odd(a) ? (r[i + 1] || 0) + 1 : 0, r), []),
getOdds = count => (a, i) => count
? left[i - 1] >= count && right[i + 1] >= count
: odd(a);
console.log(array.filter(getOdds(0)));
console.log(array.filter(getOdds(1)));
console.log(array.filter(getOdds(2)));
.as-console-wrapper { max-height: 100% !important; top: 0; }
The whole idea of functional programming is to write pure functions without side-effects
Array.filter is functional because it returns a new array, without mutating the original one. You can run that method on the same array millions of times without changing it.
If your logic gets complex, the code gets complex too, there is no functional magic which solves your domain problems.
However, you could make a createFilter function, which would create your filter function based on your domain requirements like:
const createFilter = ({
before = e => true,
after = e => true
}) => (entry, idx, entries) =>
before(entries[idx - 1]) && after(entries[idx + 1])
};
}
// This will return [ 4, 4 ] I guess ;)
[1, 3, 3, 3, 2, 4, 5, 2, 4, 7].filter(createFilter({
before: (e) => e % 2 === 0,
after: (e) => e % 2 === 1,
}))
The same way you could get only values where the item before is 50 and after 100:
[50, 1, 100, 4, 50, 3, 100].filter(createFilter({
before: (e) => e === 50,
after: (e) => e === 100
})) // pretty sure the output is [1, 3]
So this way you have a reusable filterCreator, extend it to your needs ;)
Update
#Aadit M Shah yes, after reading the OP again, I came to the conclusion, that my method would still work, you just have to write your own filterCreator function. And there is nothing wrong with the Array.filter actually.
const filterBySurrounding = (n, meetCondition) => {
return (item, idx, array) => {
return n <= idx && idx + n <= array.length - 1
? array.slice(idx - n, idx).every(meetCondition) &&
array.slice(idx + 1, idx + 1 + n).every(meetCondition)
: false
}
}
const isOdd = n => n % 2 === 1
array.filter(filterBySurrounding(50, isOdd))
Here's what I'd do:
const zipperFilter = (p, xs) => {
const before = []; // elements before x
const after = [...xs]; // x followed by elements after x, shallow copy
const result = [];
while (after.length > 0) {
const x = after.shift(); // remove x; thus, after = elements after x
if (p(before, x, after)) result.push(x);
before.unshift(x); // before = x followed by elements before x
}
return result;
};
const isOdd = n => n % 2 === 1;
const surroundedByPossiblyNOddNumbers = n => (before, x, after) =>
before.slice(0, n).every(isOdd) &&
after.slice(0, n).every(isOdd);
const surroundedByStrictlyNOddNumbers = n => (before, x, after) =>
before.length >= n &&
after.length >= n &&
before.slice(0, n).every(isOdd) &&
after.slice(0, n).every(isOdd);
const xs = [1,2,3,4,5,6,8,10,11,13,15,16,17,19];
const ys = zipperFilter(surroundedByPossiblyNOddNumbers(1), xs);
const zs = zipperFilter(surroundedByPossiblyNOddNumbers(2), xs);
const as = zipperFilter(surroundedByStrictlyNOddNumbers(1), xs);
const bs = zipperFilter(surroundedByStrictlyNOddNumbers(2), xs);
console.log(JSON.stringify(ys));
console.log(JSON.stringify(zs));
console.log(JSON.stringify(as));
console.log(JSON.stringify(bs));
What is a zipperFilter? It's a context sensitive list filter function based on the zipper data structure for lists. Any time you want to do context sensitive data processing (e.g. image processing) think of zippers.
The advantages of creating a custom zipperFilter function are:
It's more efficient than using the native filter method. This is because we don't have to keep using slice to generate the before and after arrays. We keep a running copy of both and update them on every iteration.
The before array is maintained in the reverse order. Hence, lower indices always correspond to closer neighbors. This allows us to simply slice the number of closest neighbors we want.
It's readable, generic and informs the reader that the filtering is context sensitive.
Hope that helps.
Is there an elegant, functional way to turn this array:
[ 1, 5, 9, 21 ]
into this
[ [1, 5], [5, 9], [9, 21] ]
I know I could forEach the array and collect the values to create a new array. Is there an elegant way to do that in _.lodash without using a forEach?
You could map a spliced array and check the index. If it is not zero, take the predecessor, otherwise the first element of the original array.
var array = [1, 5, 9, 21],
result = array.slice(1).map((a, i, aa) => [i ? aa[i - 1] : array[0], a]);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
An even shorter version, as suggested by Bergi:
var array = [1, 5, 9, 21],
result = array.slice(1).map((a, i) => [array[i], a]);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
A fast approach using map would be:
const arr = [ 1, 5, 9, 21 ];
const grouped = arr.map((el, i) => [el, arr[i+1]]).slice(0, -1);
console.log(grouped);
.as-console-wrapper { max-height: 100% !important; top: 0; }
This is easily done with array.reduce. What the following does is use an array as aggregator, skips the first item, then for each item after that pushes previous item and the current item as a pair to the array.
const arr = [ 1, 5, 9, 21 ];
const chunked = arr.reduce((p, c, i, a) => i === 0 ? p : (p.push([c, a[i-1]]), p), []);
console.log(chunked);
An expanded version would look like:
const arr = [1, 5, 9, 21];
const chunked = arr.reduce(function(previous, current, index, array) {
if(index === 0){
return previous;
} else {
previous.push([ current, array[index - 1]]);
return previous;
}
}, []);
console.log(chunked);
If you're willing to use another functional library 'ramda', aperture is the function you're looking for.
Example usage taken from the ramda docs:
R.aperture(2, [1, 2, 3, 4, 5]); //=> [[1, 2], [2, 3], [3, 4], [4, 5]]
R.aperture(3, [1, 2, 3, 4, 5]); //=> [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
R.aperture(7, [1, 2, 3, 4, 5]); //=> []
You may do as follows with just a sinle liner of .reduce() with no initial;
var arr = [ 1, 5, 9, 21 ],
pairs = arr.reduce((p,c,i) => i == 1 ? [[p,c]] : p.concat([[p[p.length-1][1],c]]));
console.log(pairs);
I'm sure there is an elegant way, programmatically, but, mathematically I can't help seeing that each new pair has an index difference of 1 from the original array.
If you (later) have the need to turn your array [ 1, 5, 9, 21, 33 ] into [ [1, 9], [5, 21], [9, 33] ], you can use the fact that the difference between the indices is 2.
If you create code for the index difference of 1, extending this would be easy.
Here's slide which has two parameters to control the size of the slice and how many elements are dropped between slices
slide differs from other answers here by giving you these control parameters. other answers here are limited to producing only a slices of 2, or incrementing the slice by 1 each time
// take :: (Int, [a]) -> [a]
const take = (n, xs) =>
xs.slice(0, n)
// drop :: (Int, [a]) -> [a]
const drop = (n, xs) =>
xs.slice(n)
// slice :: (Int, Int, [a]) -> [[a]]
const slide = (m, n, xs) =>
xs.length > m
? [take(m, xs), ...slide(m, n, drop(n, xs))]
: [xs]
const arr = [0,1,2,3,4,5,6]
// log helper improves readability of output in stack snippet
const log = x => console.log(JSON.stringify(x))
log(slide(1, 1, arr))
// [[0],[1],[2],[3],[4],[5],[6]]
log(slide(1, 2, arr))
// [[0],[2],[4],[6]]
log(slide(2, 1, arr))
// [[0,1],[1,2],[2,3],[3,4],[4,5],[5,6]]
log(slide(2, 2, arr))
// [[0,1],[2,3],[4,5],[6]]
log(slide(3, 1, arr))
// [[0,1,2],[1,2,3],[2,3,4],[3,4,5],[4,5,6]]
log(slide(3, 2, arr))
// [[0,1,2],[2,3,4],[4,5,6]]
log(slide(3, 3, arr))
// [[0,1,2],[3,4,5],[6]]
If for some reason you didn't want slide to include partial slices, (slices smaller than m), we could edit it as such
// slice :: (Int, Int, [a]) -> [[a]]
const slide = (m, n, xs) =>
xs.length > m
? [take(m, xs), ...slide(m, n, drop(n, xs))]
: [] // <- return [] instead of [xs]
log(slide(2, 2, arr))
// now prints: [[0,1],[2,3],[4,5]]
// instead of: [[0,1],[2,3],[4,5],[6]]
I noticed that the current solutions, in a way, all look ahead or behind (arr[i + 1] or arr[i - 1]).
It might be useful to also explore an approach that uses reduce and an additional array, defined within a function's closure, to store a to-be-completed partition.
Notes:
Not a one liner, but hopefully easy to understand
part doesn't have to be an array when working with only 2 items, but by using an array, we extend the method to work for n-sized sets of items
If you're not a fan of shift, you can use a combination of slice and redefine part, but I think it's safe here.
partitions with a length less than the required number of elements are not returned
const partition = partitionSize => arr => {
const part = [];
return arr.reduce((parts, x) => {
part.push(x);
if (part.length === partitionSize) {
parts.push(part.slice());
part.shift();
}
return parts;
}, []);
};
const makePairs = partition(2);
const makeTrios = partition(3);
const pairs = makePairs([1,2,3,4,5,6]);
const trios = makeTrios([1,2,3,4,5,6]);
console.log("partition(2)", JSON.stringify(pairs));
console.log("partition(3)", JSON.stringify(trios));