JS, difference in array matrix and forEach behavior - javascript

I was doing some training tasks for my JS course and I got one where you must implement a function that takes a positive integer (n) and returns a matrix like the one below (5 was passed):
[ [ 1, 0, 0, 0, 0 ],
[ 0, 1, 0, 0, 0 ],
[ 0, 0, 1, 0, 0 ],
[ 0, 0, 0, 1, 0 ],
[ 0, 0, 0, 0, 1 ] ]
I was able to implement the function with the following code:
function getIdentityMatrix(n) {
const mat = new Array(n).fill([]);
return mat.map((row, index) => {
row = new Array(n).fill(0);
row[index] = 1;
return row;
});
}
But while doing it I found a strange behavior that I can't explain... If I alter the code a little:
function getIdentityMatrix(n) {
const mat = new Array(n).fill(new Array(n).fill(0));
return mat.map((row, index) => {
row[index] = 1;
return row;
});
}
It returns a matrix like this:
[ [ 1, 1, 1, 1, 1 ],
[ 1, 1, 1, 1, 1 ],
[ 1, 1, 1, 1, 1 ],
[ 1, 1, 1, 1, 1 ],
[ 1, 1, 1, 1, 1 ] ]
Why would it work that way? It's like the forEach function iterates over all the elements nested inside each row which it shouldn't do.
Thank you for any advise!

It's because Array is a reference type. When you do
new Array(n).fill(new Array(n).fill(0))
first the inner new Array(n).fill(0) makes an array size n filled with 0; next the outer Array(n).fill creates an array filled with n references to that inner array.
It doesn't create n inner arrays, just n references to the same array. So when you change an element of that inner array, all the references in the outer array will reflect the change since they all point to the same object.

The code in question is equivalent to this:
let n = 5
let innerArr = new Array(n).fill(0)
function getIdentityMatrix(n) {
const mat = new Array(n).fill(innerArr);
return mat.map((row, index) => {
row[index] = 1;
return row;
});
}
console.log(getIdentityMatrix(n))
Because you are using fill you are basically filling that mat array with references to the innerArr (which you can see clearly from the above console output).
Then you do row[index] = 1 for each i which is changing the same values (at i index) of the same array.
Now your working example ... which could be written in a shorter form as:
const getIdentityMatrix = (n) =>
[...Array(n)].map((row, index) => {
row = Array(n).fill(0)
row[index] = 1
return row
})
console.log(getIdentityMatrix(3))
Clearly maps over a newly created and then spreaded array of n but then overwrites each element with an entirely new array reference.
Since that reference is brand new modifying it with row[index] = 1 produces the expected behavior when we return the x from the map.
Another way to achieve this in one line is via map, Object.assign and Object.values like this:
const gm = (n) => [...Array(n)].map((x,i) =>
Object.values(Object.assign(Object.assign({}, Array(n).fill(0)), {[i]:1})))
console.log(gm(3))

// your example is roughly equivalent to this.
const innerArray = new Array(n).fill(0);
const mat = new Array(n).fill(innerArray);
(mat[0] === mat[1] === innerArray) === true;
there is only 1 nested array, not n times array.

Related

Object.values() not updating variable

I have the variable G.playerStatsDifference defined as an array of objects:
playerStatsDifference: [{
carpenter: 0,
wood: 0,
gunman: 0,
gunpowder: 0,
merchant: 0,
gold: 0,
fleet: 0,
flagship: 0,
}, {
carpenter: 0,
wood: 0,
gunman: 0,
gunpowder: 0,
merchant: 0,
gold: 0,
fleet: 0,
flagship: 0,
}]
The point of this variable is to calculate the difference between G.playerStats which frequently changes.
My function to calculate the difference is:
const oldPlayerStats = JSON.parse(JSON.stringify(G.playerStats));
statsDifference(G, oldPlayerStats);
for (let p = 0; p < 2; p++) {
for (let s = 0; s < 8; s++) {
Object.values(G.playerStatsDifference[p])[s] = Object.values(G.playerStats[p])[s] - Object.values(oldPlayerStats[p])[s];
}
}
The expected output would be to have playerStatsDifference
When running some tests I did some console logging and it gave me the correct calculations, but the G.playerStatsDiffence would not update.
Here is some of that testing, with the calulations being correct:
console.log("Current wood is " + Object.values(G.playerStats[0])[1]); //Current wood is 5
console.log("Old wood is " + Object.values(oldPlayerStats[0])[1]); //Old wood is 10
console.log(Object.values(G.playerStats[0])[1] - Object.values(oldPlayerStats[0])[1]); //-5
I thought maybe I was doing something wrong with the loops so I tried the following afterwards:
Object.values(G.playerStatsDifference[0])[1] = Object.values(G.playerStats[0])[1] - Object.values(oldPlayerStats[0])[1];
However this did not work either. Having said that, the following does work:
G.playerStatsDifference[0].wood = Object.values(G.playerStats[0])[1] - Object.values(oldPlayerStats[0])[1];
So it seems like I have some issue with the Object.values on G.playerStatsDifference. Any idea on why that is and how I can run that through the loop?
=====
EDIT: As those in the comments have pointed out my question is a bit confusing so I will try to clear it up here..
The G.playerStatsDifference value is supposed to track the difference between the previous value of G.playerStats and the current value of G.playerStats.
To do this I am setting the value of oldPlayerStats to equal G.playerStats and then updating G.playerStats to its new value.
I then need to run through the array of objects and subtract the value of G.playerStats from oldPlayerStats. This will produce the value of G.playerStatsDifference
That is what the loop is for, to go through each object key and do the calculation.
Hope this provides some clarity. Sorry for the poorly worded question.
const diffBetweenObjectValues = (a, b) => {
return Object.entries(a).reduce((result, [aKey, aVal]) => {
result[aKey] = aVal - (b[aKey] ?? 0);
return result;
}, {});
}
const stats = { a: 1, b: 2 };
const updatedStats = { a: 1, b: 1 };
// Initial player stats are { a: 1, b: 2 }
const player = { stats: stats, diff: {} };
// Set the diff, value is { a: 0, b: 1 }
player.diff = diffBetweenObjectValues(player.stats, updatedStats);
// Actually update the stats, value is { a: 1, b: 1 }
player.stats = updatedStats;
Note that if a key is present in b but not a it's ignored. Also note that this only works properly if all the property values are numeric.
You can put the state transition in a function and just run it when you need to update the stats (like every tick of the game loop).
Response to comment
Ok, lets add another helper function
const zip = (a, b) => a.map((x, i) => [x, b[i]]);
const players = [...]; // array of players
const statUpdates = [...]; // array of stat updates
zip(players, statUpdates).forEach(([player, stats]) => {
player.diff = diffBetweenObjectValues(player.stats, stats);
player.stats = stats;
});
Zip combines the array of players and the array of stat updates in to pairs, then iterate over them with forEach, destructure the bits back out, and run the update. You can also just use a for loop, which is faster but harder to read and easier to get wrong (e.g. off-by-one errors). I would stick with the version until/unless your profiler tells you it's too slow.
Update 2
const currentStats = [{ a: 1, b: 2 }, {a: 3, b: 2 }];
const updatedStats = [{ a: 0, b: 1 }, {a: 4, b: 1 }];
const diffedStats = zip(currentStats, updatedStats).map(([current, updated]) => {
return diffBetweenObjectValues(current, updated);
});
// for testing purposes, create an object with some random stats
const randomPlayerStats = () => Object.fromEntries(
['carpenter','wood','gunman','gunpowder','merchant','gold','fleet','flagship']
.map(k=>[k,Math.random()*10|0]));
// array of the last player stats recorded for each player
let lastPlayerStats = [];
// create a new object from the existing object, subtracting each entry
// from the old object from the entry from the new object
// note: uses the ?? operator so that if there is no last object yet,
// the last object value will be treated as being zero
const difference = (playerStats, lastPlayerStats) => {
let r = Object.fromEntries(Object.entries(playerStats).map(([k,v])=>
[k, v-(lastPlayerStats?.[k]??0)]));
lastPlayerStats = playerStats;
return r;
};
// simulate 5 rounds of the game, with 2 players in the game
const playerCount = 2;
const simulatedRounds = 5;
for(let c=0;c<simulatedRounds;c++) {
let playerStats = [...Array(playerCount).keys()].map(i=>randomPlayerStats());
let playerStatsDifference = playerStats.map((s,i)=>
difference(s, lastPlayerStats[i]??{}));
console.log('playerStats:');
console.log(playerStats);
console.log('playerStatsDifference:');
console.log(playerStatsDifference);
}

In Javascript how do I create a secondary array in a specific index?

How do I create a subarray from an existing array in Javascript?
For example;
Arr = [5,2,1,2]
Then I want to insert 8 in position 1 of Arr, but keep the original value 2. So arr can become:
Arr = [5,[2,8],1,2]
I tried using concat, which sort of did something but duplicates all values.
Bear in mind that this can grow e.g. Arr = [5,[2,8,3,4],1,[2,3]]
Thanks!
You could assign the concatinated values.
const
addAt = (array, value, index) => array[index] = [].concat(array[index], value),
array = [5, 2, 1, 2];
addAt(array, 8, 1);
console.log(array)
addAt(array, 3, 1);
console.log(array)
.as-console-wrapper { max-height: 100% !important; top: 0; }
There are several different ways to do this, but you can reassign the current array index to an array like so:
Arr[1] = [Arr[1], 8]. Then if you wanted to continue adding to the array at index 1 in Arr, you could do something like Arr[1].push(x).
You could do something like this (Probably not the best answer, but may be helpful)
const addIntoArray = (arr, toAdd, index) => {
arr[index] = typeof arr[index] == "number" ? [arr[index], toAdd] : [...arr[index], toAdd];
return arr
}
Arr = [5,2,1,2]
console.log(Arr); // [ 5, 2, 1, 2 ]
addIntoArray(Arr, 1, 1)
console.log(Arr); // [ 5, [ 2, 1 ], 1, 2 ]
addIntoArray(Arr, 3, 1)
console.log(Arr) // [ 5, [ 2, 1, 3 ], 1, 2 ]
Basically ...arr expands the array and then we add toAdd at it's end and [arr[index], toAdd] creates an array with the first element as the number and the second the new element. (It modifies the original array, as shown in the example, so pay attention as it may lead to bugs)
The typeof arr[index] == "number"is just a simple/generic typecheck to see if there isn't an array already
This function should satisfy your conditions at basic level
// a - array, v - value to insert, i - position to insert
const avi = (a, v, i) => {
r = a.slice(0, i);
r.push([a[i], v]);
a.slice(i+1, a.length).forEach(e => r.push(e));
return r;
}
console.log(JSON.stringify(avi([5,2,1,2], 8, 1)))
//=> "[5,[2,8],1,2]"

Could not perform a recursive multidimensional array

I am making a recursive multidimensional array in javascript. But with a matrix I find it difficult.
For example, when I do this: matrix([2,3,4]) I want it to return this to me:
[ [ [ 0, 1, 2, 3 ]
, [ 0, 1, 2, 3 ]
, [ 0, 1, 2, 3 ]
]
, [ [ 0, 1, 2, 3 ]
, [ 0, 1, 2, 3 ]
, [ 0, 1, 2, 3 ]
] ]
The length of the entered matrix must be the number of dimensions and the numbers must be the value of the dimensions, having a 3D 2x3x4 matrix (height, width and height).
Code:
function copyArray(A)
{
var B=[]
for(var i=0;i<A.length;i++)
{
B[i]=A[i]
}
return B
}
function matrix(dims)
{
var I=dims[0]
dims.shift()
var A=[]
A.length=I
for(var i=0;i<I;i++)
{
var dims2=copyArray(dims)
A[i]=matriz(dims)
dims=dims2
}
return A
}
The code I have generates the following error:
Uncaught RangeError: Invalid array length(…)
You can do it this way, but it should be mentioned first:
Array(length): to create an array of the specified length.
.shift(): to remove the first element from the array.
dims.length ?: to see if the recursive function should still be
executed.
dims.slice(0): to clone the array passed to the function.
function matrix(dims) {
var arr = Array(dims.shift() || 0);
for(var idx = 0; idx < arr.length; idx++) {
arr[idx] = dims.length ? matrix(dims.slice(0)) : idx;
}
return arr;
}
console.log( matrix([2,3,4]) )
.as-console-wrapper {max-height: 100%!important;top:0}
Use a recursive function for this... build the level according to the dimension remove the index of the level you have dealt and move foward... do this until there no more dimensions to be handled.
This is an example of how to do it...
const createRange = (FROM, TO) => [...Array(TO - FROM + 1).keys()].map(i => i + FROM);
const createMatrix = (dimensions) => {
const dim = dimensions[0];
const newDimensions = dimensions.slice(1, dimensions.length);
if (!newDimensions.length) return createRange(0, dim - 1);
return [...Array(dim).keys()]
.map(_ => createMatrix(newDimensions));
};
console.log(
createMatrix([2,3,4])
)
Another approach using Array.from() and it's built in mapper
const matrix = (dims) => (
Array.from({length: dims.shift()}, (_,i) => dims.length ? matrix([...dims]) : i)
)
console.log(matrix ([2,3,4]))

Attempting to set single value at Javascript array index. It is mass-assigning values instead

I am working on a matrix algorithm but have run into a problem early on.
I have the following array:
[ [ 0, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ]
I want to transform it into this:
[ [ 0, 0, 0 ], [ 0, 9, 0 ], [ 0, 0, 0 ] ]
I am attempting to set the middle value like this:
array[1][1] = 9
In an isolated context this works fine. However, in the context of my recursive loop it is not working, and I wind up with this instead:
[ [ 0, 9, 0 ], [ 0, 9, 0 ], [ 0, 9, 0 ] ]
So my question is where have I gone wrong in my program?
function matrix(n, array = initArrays(n), i = 0, j = 0) {
if (i > 0) {
return array // <--- returns: [ [ 0, 9, 0 ], [ 0, 9, 0 ], [ 0, 9, 0 ] ]
}
array = addDigit(n, array, i, j)
return matrix(n, array, i + 1, j)
}
function initArrays(n) {
const array = []
const subArray = []
for (let i = 0; i < n; i++) {
subArray.push(0)
}
for (let i = 0; i < n; i++) {
array.push(subArray)
}
return array
}
function addDigit(n, array, i, j) {
// array = [ [ 0, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ] // <--- uncommenting this line fixes the problem. Why?
array[1][1] = 9
return array
}
matrix(3)
This is a simplified, contrived version of my program. It presently only recurs a single time, and sets the middle value of a 3x3 grid (ignore j, it is intended for future functionality).
It can be seen in action here:
Non-working: What I want to fix
https://repl.it/repls/AttentiveWideOrder
Working: Hack solution
https://repl.it/repls/CuteSoggyArchitects
The way the hack solution works is to manually reset the array immediately before attempting to assign my middle value. I have no idea why this makes any difference and would appreciate anyone who can point out my fault.
Thanks.
You need to push independent arrays to the outer array. Javascript uses an object reference and you push the same reference to the outer array.
function initArrays(n) {
const array = []
const subArray = []
for (let i = 0; i < n; i++) {
subArray.push(0)
}
for (let i = 0; i < n; i++) {
array.push(subArray.slice()); // get copy of primitive values
}
return array
}
var array = initArrays(3);
array[1][1]= 9;
console.log(array);

Convert JSON data into format in Highcharts' basic line chart

Using Javascript, I am trying to convert some JSON data into the format used in Highcharts' basic line chart.
What I have to start with:
originalArray = [
['valueA', 1],
['valueA', 0],
['valueB', 9],
['valueB', 9],
['valueB', 3],
['valueC', 11]
]
And what I'm trying to create using the above:
desiredArray = [{
name: 'valueA',
data: [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}, {
name: 'valueB',
data: [0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 0, 0]
}, {
name: 'valueC',
data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
}]
For some additional context, the 0-11 in originalArray[i][1] references a month (0 = January), and the desiredArray is a list of unique names and a count of their occurrences by month.
So far, I can:
Convert the data into a new array of objects
For each unique name in originalArray
Create a new object in the desiredArray, and set the name attribute
Add a data attribute that contains an empty array
But then I run into trouble, and can't figure out how to:
Loop through the originalArray
If the name in the originalArray matches the name in the desiredArray
Increment a counter in the matching seriesArray[i].data array, using the value of originalArray[i][1] as the index (it always be 0-11).
So I'm asking:
What's a good way to iterate across my originalArray, match up unique values, and then act only on those matches to push to the desiredArray.
What's the best way to increment the counters in desiredArray[i].data
I'm open to using libraries, such as underscore.js. Have been trying to figure this out for a couple of days now, so pretty much anything goes within the bounds of Javascript.
Updated with proper array initialization, now.
var max = originalArray.reduce(function(p,c){return Math.max(p,c[1]);},0);
var initSums = function(size) {
var arr = new Array(size);
for (var i=0;i<size;i++)
arr[i]=0;
return arr;
}
var map = originalArray.reduce(function(sums,val){
if (!sums.hasOwnProperty(val[0])) {
sums[val[0]] = initSums(max+1);
}
sums[val[0]][val[1]]++;
return sums;
},{});
var desiredArray = Object.keys(map).map(function(key) {
return {name: key, data: map[key]};
});
What we're doing here is a multi-step process:
Decide how big our arrays are going to need to be, by first scanning for the largest value in the original array.
Use an object to aggregate the counts (using Array.reduce()).
Transform the object and its properties into an array of name/data pair objects (using Array.map).
Edit: An improvement on S McCochran's solution, skipping the extraneous search for the maximum value in originalArray, since there should always be 12 elements of each data array, one per month.
function formatForHighcharts(array) {
// Create a map from value name to array of month counts
var map = originalArray.reduce(function(sums, pair) {
var key = pair[0], val = pair[1];
if(!(key in sums))
sums[key] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
// val is the month index, which corresponds directly to array index, so increase that
sums[key][val]++;
return sums;
}, {});
// Map the object to an array of { name: ..., data: ... } pairs
var formatted = Object.keys(map).map(function (key) {
return { name: key, data: map[key] };
});
return formatted;
}
Usage:
var desiredArray = formatForHighcharts(originalArray);

Categories

Resources