1

I'm still getting my head around Apps Script and just need some help editing an array.

So I have two arrays:

var arrayBig = SpreadsheetApp.getSheetByName('Big').getDataRange().getValues();

var arraySmall = SpreadsheetApp.getSheetByName('Small').getDataRange().getValues();

And I want to replace specific rows in arrayBig based on values in arraySmall, and then write that array back to my spreadsheet (the last part I'm ok with).

The two arrays have the same number of columns, but arraySmall has fewer rows.

The rows to replace:

  • Based on the numeric value in Column 1 in arraySmall replace that row in arrayBig.

So if the value in Column 1 = 3 replace row number 3 in arrayBig with the contents of the row from arraySmall where Column 1 = 3.

I think the answer has to do with the map method but I don't understand it. Just some pointers to get me started would be much appreciated, thank you.

TopMarx
  • 77
  • 8

2 Answers2

1

Here is one "get you started" approach:

You describe your data, but you do not provide any concrete samples - so here is my starting data, making some assumptions based on the information in the question:

var arrayBig = [ [1, "A", "B"], [2, "C", "D"], [3, "E", "F"], [4, "G", "H"] ];
var arraySmall  = [ [1, "P", "Q"], [3, "Y", "Z"] ];

With the above data, the expected outcome is the following, where rows 1 and 3 are replaced:

[ [1, "P", "Q"], [2, "C", "D"], [3, "Y", "Z"], [4, "G", "H"] ];

Here is the approach:

var arrayBig = [ [1, "A", "B"], [2, "C", "D"], [3, "E", "F"], [4, "G", "H"] ];
var arraySmall  = [ [1, "P", "Q"], [3, "Y", "Z"] ];

var mapper = new Map();
arraySmall.forEach((row) => { 
  mapper.set(row[0], row);
} );

newArrayBig = [];
arrayBig.forEach((row) => { 
  if ( mapper.has( row[0] ) ) {
    newArrayBig.push( mapper.get( row[0] ) );
  } else {
    newArrayBig.push( row );
  }
} );

console.log( newArrayBig );

This assumes you have an iron-clad guarantee that there are never more rows of data in arraySmall than there are in arrayBig. You can (and should) add some logic to test for that, for more robust code.

Notes

The Map object (not the map() method) is central to this approach. This object provides a lookup structure: an index value, pointing to a data value:

var mapper = new Map();
arraySmall.forEach((row) => { 
  mapper.set(row[0], row);
} );

In our case, the index is the number in the first cell of each arraySmall row. And the value it points to is the complete data array for that row, for example:

1 -> [1, "P", "Q"]
3 -> [3, "Y", "Z"]

We can now use this lookup data as we iterate through each row of our arrayBig data:

arrayBig.forEach((row) => { ... } );

The logic inside this forEach iterator basically says: If the map contains an array using the same number as the current arrayBig row, then use the arraysmall data. Otherwise, use the arrayBig data.

The newArrayBig.push() method adds that array to our results array.

You can then write this back to your spreadsheet, as needed.

andrewJames
  • 19,570
  • 8
  • 19
  • 51
  • Thank you @andrewjames, you've understood what I want exactly. To confirm: "this assumes you have an iron-clad guarantee that there are never more rows of data in arraySmall than there are in arrayBig." - that is correct, possibly an equal number but never more. I look forward to getting to grips with your answer and applying it to my spreadsheet, thank you! – TopMarx Jul 17 '21 at 01:39
  • Thank you again, I'm playing around to try to understand the function; changing the index column and changing the order of the arraySmall - and it's working really well. I like that it's basically an Apps Script version of an index(match) formula (or vlookup). So the arraySmall doesn't have to be sequential, for instance `var arraySmall = [ [3, "Y", "Z"], [1, "P", "Q"] ];` also works fine. I don't quite understand Map() and Map().set(key, value) I mean I can see what happens, I just don't get them exactly. Do you know of a good documentation link? thank you! – TopMarx Jul 17 '21 at 02:48
  • Conceptually, you are correct, a JavaScript map is like a vlookup. A phone directory is a type of map: It lets you use a key (a person's name) to quickly find a value (their phone number). When you use `set()` you are adding an entry to the phone directory (or replacing the existing entry): `myDirectory.set('Smith', '123-45-67')`. And when you use `myDirectory.get('Smith')`, you retrieve the phone number. This retrieval is efficient/fast, compared to searching an array. – andrewJames Jul 17 '21 at 13:39
  • Documentation: I often use [this](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), but there are many others, such as [this](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), which is part of an [overall tutorial site](https://javascript.info/). You should try different resources and find the ones that you like the best. As you can see from the answer by @YuriKhristich, there are ways to combine functions and operators in JavaScript which can make your code very compact - which is great. – andrewJames Jul 17 '21 at 13:39
  • 1
    Thank you, really clear. So far in my limited experience using Apps Script I've used `setValues()` or `getValues()` to write to the sheet, rather than use JavaScript maps. But now I've a need to do something a little more complex (well, complex for me!) and this is much faster. Thank you for the links as well. – TopMarx Jul 17 '21 at 15:07
  • OK - but be careful here. Google Apps scripts use JavaScript as its core programming language - but Google has added a whole [library of extra stuff](https://developers.google.com/apps-script/reference/spreadsheet) which is specifically there to let you interact with the spreadsheet - and that is why you need [`getValues()`](https://developers.google.com/apps-script/reference/spreadsheet/range#getValues()) and so on. – andrewJames Jul 17 '21 at 15:42
  • Once you have extracted your data from the sheet into a JavaScript variable, you can use all these great JavaScript techniques to manipulate the data, before you write it back to the sheet using Google's `setValues()` command. So, it's always good to understand when you are using "pure" JavaScript (e.g. for data manipulation) and when you are using Google's functions (e.g. for interacting with the spreadsheet). – andrewJames Jul 17 '21 at 15:43
  • That all makes sense @andrewjames. I have another follow-up question: is there a way to match two keys/indices? For instance, say my arraySmall is `var arraySmall = [ [1, "A", "Q"], [3, "Y", "Z"] ];` and I want to match the first two values: `if ( mapper.has( row[0] ) && mapper.has( row[1] ) )` so that the final expected result is `[ [1, "A", "Q"], [2, "C", "D"], [3, "E", "F"], [4, "G", "H"] ];` is that possible? And to be clear one match value is a number the other is text. Or is a completely different approach required? Thank you – TopMarx Jul 18 '21 at 14:26
  • You can use the same approach - but when you use `mapper.set(key, value);`, the key can no longer be `row[0]`. It has to be a combination of `row[0]` and `row[1]` - for example `mapper.set(row[0].toString + '-' + row[1], row);`. And, of course when you use `mapper.has( key )` and `mapper.get( key )`, you need to build the key in the same way, from the `arrayBig` data. The key is the "unique identifier" - whether that be the first field, the first two fields, or something different. – andrewJames Jul 18 '21 at 14:41
  • That works, thank you! So, because I'm learning, what that does is create a string of the first two fields separated by a dash? (but you don't necessarily need the dash?) EDIT just tested `row[0].toString + row[1]` works fine too – TopMarx Jul 18 '21 at 14:53
  • Yes - it creates a string. You might not need the dash _in your specific case_. But I included it deliberately, as a safety precaution. Consider if your data includes two rows as follows: `[ 1, "11", "P" ]` and `[ 11, "1", "Q" ]`. What are the keys, if there is no dash vs. if there is a dash? Keys need to be unique. – andrewJames Jul 18 '21 at 14:57
  • Ah yes, makes sense, if both fields are numbers the dash is needed to keep them as separate numbers rather than concatenating or adding (one or the other would happen I guess) – TopMarx Jul 18 '21 at 15:02
  • Hi @andrewjames, I've noticed an unintended result with the `mapper.set(row[0].toString + '-' + row[1], row);` it's not checking the per row pair but rather if one field or the other is present. So if my arraySmall is `var arraySmall = [ [1, "A", "Q"], [3, "A", "Z"] ];` the result I get is `[ [ 3, 'A', 'Z' ], [ 2, 'C', 'D' ], [ 3, 'E', 'F' ], [ 4, 'G', 'H' ] ]` but [3, 'A', 'Z'] is not correct because there isn't a `[3,'A','Letter']` in the arrayBig, there is a `[1,'A','B']` and a `[3,'E','F']`. From arraySmall only `[1,'A','Q']` should be added to arrayBig. – TopMarx Jul 18 '21 at 21:37
  • Is it possible to tweak the code so it work only if both fields match? Thank you – TopMarx Jul 18 '21 at 21:44
  • Sorry - the syntax is `.toString()` not `.toString`. A mistake on my part. Does that help? – andrewJames Jul 18 '21 at 22:01
  • Yes it does! thank you. Why did it make a difference? – TopMarx Jul 18 '21 at 22:34
  • When you use `x.toString()` that is how you execute the [`toString`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString) method (and how you generally execute any method). It converts `123` into `"123"`. But when you use `x.toString` that returns a reference to the _definition_ of the method, instead of executing it. A foolish mistake on my part. Read more [here](https://stackoverflow.com/questions/7629891/functions-that-return-a-function). – andrewJames Jul 18 '21 at 22:48
1

Just in case. Another solution:

var arrayBig    = [ [1,"A","B"], [2,"C","D"], [3,"E","F"], [4,"G","H"] ];
var arraySmall  = [ [1,"P","Q"], [3,"Y","Z"] ];

var obj = {}; // it will be { "1":[1,"P","Q"], "3": [3,"Y","Z"] }

arraySmall.forEach( x => obj[x[0]] = x );

arrayBig = arrayBig.map( x => obj[x[0]] || x );

console.log(arrayBig); // [[1,"P","Q"], [2,"C","D"], [3,"Y","Z"], [4,"G","H"]];

It converts the small array into an object where first element of every row is a key and the row is its value. Then it loops through the big array and tries to get values of the object by keys (a first cell). If the key exists the row will replace, if key doesn't exist the for wont change.

Sequence of the rows in the small array doesn't matter as well.

Updage

You can use an array instead of an object if you want:

var arrayBig = [ [1,"A","B"], [2,"C","D"], [3,"E","F"], [4,"G","H"] ];
var arraySmall  = [ [1,"P","Q"], [3,"Y","Z"] ];

var arr = [];

arraySmall.forEach( x => arr[x[0]-1] = x ); // [ [1,"P","Q"], [], [3,"Y","Z"], [] ];

arrayBig = arrayBig.map( (x,i) => arr[i] || x);

console.log(arrayBig);

I think it will take more memory, but it works. But you need to be sure that first cells contain numbers only, since they're using as indexes of the array (indexes of array should be numbers, unlike keys that can be anything). And rows of first array should be sorted and has no skipped numbers (1, 2, 3, 4, etc). So, it's a more fragile solution. Just for educational purposes.

Yuri Khristich
  • 13,448
  • 2
  • 8
  • 23
  • Thank you @YuriKhristich, yes this also works. I'm curious about using an object, as opposed to an array (I'm not familiar with objects!), and also how arrayBig.map() is working. It looks like a different way of writing 'if else' that I'm not familiar with either. – TopMarx Jul 17 '21 at 12:23
  • In this case the object is like an array, but you you can get elements of the 'array' via 'keys' not via indexes. So `obj[key] = value` is the same as `array[index] = value`. First cells of the rows of your array are keys. Rows are values. `arrayBig.map()` is just a short variant of a loop. It can be replaced with: `for (i=0;i – Yuri Khristich Jul 17 '21 at 12:35
  • Interesting, thank you for the explanation – TopMarx Jul 17 '21 at 13:42