Google Sheets script - find matching tuples and copy data between sheets - google-apps-script

I have 2 sheets in the same spreadsheet, call them sheet1 and sheet2. In each sheet, every row describes some hardware component and its properties. The point of sheet2 is to eventually replace the outdated sheet1.
Simple example, (real sheets are hundreds of lines long):
sheet1:
componentId | prop1 | prop2 | prop3 | isvalid
---------------------------------------------
1 | x1 | y1 | z1 | yes
2 | x1 | y2 | z3 | yes
3 | x2 | y1 | z1 | yes
sheet2:
componentId | quantity | prop1 | prop2 | prop3 | prop4 | isvalid
----------------------------------------------------------------
15 | 4 | x1 | y1 | z1 | w1 | TBD
23 | 25 | x3 | y3 | z2 | w1 | TBD
33 | 3 | x1 | y2 | z3 | w2 | TBD
The final column "isValid" in sheet1 has been manually populated. What I would like to do is write a script that iterates through sheet1, producing a tuple of the property values, and then looks for matching property value tuples in sheet2. If there is a match, I would like to copy the "isValid" field from sheet1 to the "isValid" field in sheet2.
What I have so far is the following, but I am experiencing a error "The coordinates or dimensions of the range are invalid" - see comment in code below showing where error is. And, the entire thing feels really hacky. Was hoping someone could maybe point me in a better direction? Thanks in advance.
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length != b.length) return false;
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
function copySheetBasedOnRowTuples(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet1 = ss.getSheetByName('sheet 1 name');
var sheet2 = ss.getSheetByName('sheet 2 name');
s2data = sheet2.getDataRange().getValues()
s1data = sheet1.getDataRange().getValues()
for( i in s1data ){
sheet1Tuple = [ s1data[i][1], s1data[i][2], s1data[i][3] ]
// Now go through sheet2 looking for this tuple,
// and if we find it, copy the data in sheet1 column 4
// to sheet2 column 6 for the rows that matched (i and j)
for ( j in s2data){
sheet2Tuple = [ s2data[j][2], s2data[j][3], s2data[j][4] ]
if ( arraysEqual(sheet1Tuple, sheet2Tuple) ){
// ERROR HAPPENS HERE
sheet2.getRange(j, 6).setValue( sheet1.getRange( i, 4 ).getValue() )
}
}
}
}

The reason of error is the start number between array and range. The index of array starts from 0. The row and column of getRange() start from 1. So how about this modification?
From :
sheet2.getRange(j, 6).setValue( sheet1.getRange( i, 4 ).getValue() )
To :
sheet2.getRange(j+1, 7).setValue( sheet1.getRange( i+1, 5 ).getValue() )
If this was not useful for you, please tell me. I would like to modify.

Related

Why isn't array Intersection in google spreadsheet working as expected?

This is my first time using google spreadsheets, and I'm trying to accomplish something in the functions. I know it's javascript (A language I'm not entirely familiar with). I'm trying to take a range from two tables, and get the intersection location of each, and fill in missing values for rows that already should exist.
For example, assuming the following two sheets:
Sheet1
+-----------+----------+--------+---------+
| Fruit | Color | Weight | isApple |
+-----------+----------+--------+---------+
| Banana | Yellow | 3 | no |
| Orange | Orange | 3 | no |
| Apple | Red | 2 | yes |
| Pineapple | Brownish | 5 | no |
+-----------+----------+--------+---------+
Sheet2
+-----------+----------+--------+---------+
| Fruit | Color | Weight | isApple |
+-----------+----------+--------+---------+
| Banana | | | |
| Apple | | | |
| Pear | | | |
| Watermelon| | | |
+-----------+----------+--------+---------+
I want to find the intersections of the Fruit row, and fill in the color, weight, and isApple, of each one we know.
I wrote something I felt confident should work to get the intersection of range1, and it returns an empty array for some reason. When I test this in Apps Script Editor, it seems to work fine.
//assuming range1 = Sheet1!A2:A5 and range2=Sheet2!A2:A5
function intersection(range1, range2) {
var i = 0;
var j = 0;
var matches = new Array();
while(i < range1.length){
if(range2.toString().includes(range1[i].toString())){
matches.push(i);
}
i++
}
return matches;
}
I would expect this to return an array of [0,2] since the 0th element Banana, and the 1st element Apple from Sheet1, exist in sheet 2.
I would then use that data to fill in the rows for Apple, and Banana from the information in Sheet2.
I'm not at that second part yet, since I can't seem to get the sheet to even find the intersection.
My end goal expected output would be that Sheet 2 is changed to:
Sheet2
+-----------+----------+--------+---------+
| Fruit | Color | Weight | isApple |
+-----------+----------+--------+---------+
| Banana | Yellow | 3 | no |
| Apple | Red | 2 | yes |
| Pear | | | |
| Watermelon| | | |
+-----------+----------+--------+---------+
I believe your goal as follows.
You want to achieve the result from "Sheet1" and "Sheet2" as shown in your question.
You want to achieve this using the custom function.
For this, how about this answer?
Modification points:
In your script, I thought that you are using the custom function of =intersection(Sheet1!A2:A5,Sheet2!A2:A5). In this case, the arguments of range1 and range2 of function intersection(range1, range2) { are [["Banana"],["Orange"],["Apple"],["Pineapple"]] and [["Banana"],["Apple"],["Pear"],["Watermelon"]], which are 2 dimensional arrays, respectively. Namely, the values on the sheet are sent to the arguments. In order to achieve your goal, I would like to propose to use the custom function like =intersection(Sheet1!A2:D5,Sheet2!A2:A5). By this, the values from "Sheet1" can be used in the custom function.
In your script, the value of [0,2] is retrieved as matches. This is the indexes of range1. But in this case, it is required to also know the indexes matching to range2. This has already mentioned in your question. In this case, how about the following flow?
In order to achieve your goal, I would like to propose the following flow.
From the values of "Sheet1", create an object for searching values of the column "A".
From the values of "Sheet2", create an result array using the created object.
The sample script reflected above flow is as follows.
Sample script:
Please copy and paste the following script, and out the custom function =intersection(Sheet1!A2:D5,Sheet2!A2:A5) to the cell "B2" of "Sheet2". By this, the result that you showed at the bottom of your question is obtained.
function intersection(values1, values2) {
const obj = values1.reduce((o, [a, ...bcd]) => Object.assign(o, {[a]: bcd}), {});
return values2.map(([a]) => obj[a] ? obj[a] : [""]);
}
Result:
References:
Custom Functions in Google Sheets
reduce()
Object.assign()
Spread syntax (...)
map()

How to pass a cell reference to an Apps Script custom function?

Assuming that:
A1 = 3
B1 = customFunc(A1) // will be 3
In my custom function:
function customFunc(v) {
return v;
}
v will be 3. But I want access the cell object A1.
The following is transcribed from the comment below.
Input:
+---+---+
| | A |
+---+---+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
+---+---+
I want to copy A1:A4 to B1:C2 using a custom function.
Desired result:
+---+---+---+---+
| | A | B | C |
+---+---+---+---+
| 1 | 1 | 1 | 2 |
| 2 | 2 | 3 | 4 |
| 3 | 3 | | |
| 4 | 4 | | |
+---+---+---+---+
To achieve the desired result of splitting an input list into multiple rows, you can try the following approach.
function customFunc(value) {
if (!Array.isArray(value)) {
return value;
}
// Filter input that is more than a single column or single row.
if (value.length > 1 && value[0].length > 1) {
throw "Must provide a single value, column or row as input";
}
var result;
if (value.length == 1) {
// Extract single row from 2D array.
result = value[0];
} else {
// Extract single column from 2D array.
result = value.map(function (x) {
return x[0];
});
}
// Return the extracted list split in half between two rows.
return [
result.slice(0, Math.round(result.length/2)),
result.slice(Math.round(result.length/2))
];
}
Note that it doesn't require working with cell references. It purely deals with manipulating the input 2D array and returning a transformed 2D array.
Using the function produces the following results:
A1:A4 is hardcoded, B1 contains =customFunc(A1:A4)
+---+---+---+---+
| | A | B | C |
+---+---+---+---+
| 1 | a | a | b |
| 2 | b | c | d |
| 3 | c | | |
| 4 | d | | |
+---+---+---+---+
A1:D4 is hardcoded, A2 contains =customFunc(A1:D4)
+---+---+---+---+---+
| | A | B | C | D |
+---+---+---+---+---+
| 1 | a | b | c | d |
| 2 | a | b | | |
| 3 | c | d | | |
+---+---+---+---+---+
A1:B2 is hardcoded, A3 contains =customFunc(A1:B2), the error message is "Must provide a single value, column or row as input"
+---+---+---+---------+
| | A | B | C |
+---+---+---+---------+
| 1 | a | c | #ERROR! |
| 2 | b | d | |
+---+---+---+---------+
This approach can be built upon to perform more complicated transformations by processing more arguments (i.e. number of rows to split into, number of items per row, split into rows instead of columns, etc.) or perhaps analyzing the values themselves.
A quick example of performing arbitrary transformations by creating a function that takes a function as an argument.
This approach has the following limitations though:
you can't specify a function in a cell formula, so you'd need to create wrapper functions to call from cell formulas
this performs a uniform transformation across all of the cell values
The function:
/**
* #param {Object|Object[][]} value The cell value(s).
* #param {function=} opt_transform An optional function to used to transform the values.
* #returns {Object|Object[][]} The transformed values.
*/
function customFunc(value, opt_transform) {
transform = opt_transform || function(x) { return x; };
if (!Array.isArray(value)) {
return transform(value);
}
// Filter input that is more than a single column or single row.
if (value.length > 1 && value[0].length > 1) {
throw "Must provide a single value, column or row as input";
}
var result;
if (value.length == 1) {
// Extract single row from 2D array.
result = value[0].map(transform);
} else {
// Extract single column from 2D array.
result = value.map(function (x) {
return transform(x[0]);
});
}
// Return the extracted list split in half between two rows.
return [
result.slice(0, Math.round(result.length/2)),
result.slice(Math.round(result.length/2))
];
}
And a quick test:
function test_customFunc() {
// Single cell.
Logger.log(customFunc(2, function(x) { return x * 2; }));
// Row of values.
Logger.log(customFunc([[1, 2, 3 ,4]], function(x) { return x * 2; }));
// Column of values.
Logger.log(customFunc([[1], [2], [3], [4]], function(x) { return x * 2; }));
}
Which logs the following output:
[18-06-25 10:46:50:160 PDT] 4.0
[18-06-25 10:46:50:161 PDT] [[2.0, 4.0], [6.0, 8.0]]
[18-06-25 10:46:50:161 PDT] [[2.0, 4.0], [6.0, 8.0]]

Sort Array by highest value google script

I have the following spreadsheet:
| Name | Rating |
| John | 2000 |
| Jane | 50 |
| Bill | 3000 |
| Sam | 500 |
I get the data for the spreadsheet in the following way:
mySheet.getDataRange().getValues();
My goal is to print the array from highest rating to lowest rating. How can I do that?
EDIT: I probably should mention I am aware of the sort function: However, the issue there is it sorts as a string, not a number. Which means that 60 will show above 2000 because it starts with a 6.
You need to pass a comparator function to the sort() method.
function sortDescending() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var range = sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn());
var values = range.getValues();
values.sort(function(a, b){
return b[1] - a[1]; //sorts by the 2nd element in a row
});
Logger.log(values);
}
Note that I skipped the header row and didn't include it in the list of values to sort. Hope this helps.

Create new row for each not empty column

I'm trying to convert an Excel macro to Google Apps Script. I would like to create a new row on a specific sheet for each not empty column in Google Spreadsheets.
My Inputsheet looks like the following:
ID | Inrellevant Column | Givenmoney | Takenmoney | Othermoney
1 | Data1 | 100 | 200 | 300
2 | Data2 | 400 | | 500
I want to create a new row in another sheet for each not empty cell, so the desired Outputsheet would be:
ID | Inrellevant Column | Moneycode | Amount
1 | Data1 | Givenmoney | 100
1 | Data1 | Takenmoney | 200
1 | Data1 | Othermoney | 300
2 | Data2 | Givenmoney | 400
2 | Data2 | Othermoney | 500
I tried the following:
Outputsheet.getRange('A2').offset(0, 0, Inputsheet.length).setValues(Inputsheet);
However I can't see to create a loop to create new rows for each not empty column.
Hoi Fred, assuming you want the output to appear from the top left cell onwards in the sheet 'Outputsheet', try this code:
function myFunction() {
var ss = SpreadsheetApp.getActive(),
source = ss.getSheetByName('Inputsheet'),
target = ss.getSheetByName('Outputsheet'),
arr = [
["ID", "Header 2nd col", "Moneycode", "Amount"]
],
data = source.getDataRange().getValues(),
headers = data[0];
data.splice(1)
.forEach(function (r) {
r.forEach(function (c, i) {
if (!isNaN(parseFloat(c)) && isFinite(c) && i > 1) {
arr.push([r[0], r[1], headers[i], c])
}
})
})
target.getRange(1, 1, arr.length, arr[0].length).setValues(arr);
}
See this example sheet where you can run the above script from the menu 'My Menu'....

Google sheets, sort function

I want to auto-sort through the script editor in Google Sheets. I have in my Google Sheets several columns:
+--------+------------+-------+--------+------------+
| Region | Mag | Comp | Region | Mag |
+--------+------------+-------+--------+------------+
| A | MIKA | TRUE | A | MIKA |
| B | KALO | FALSE | B | NOKA |
| C | MINA | FALSE | C | South-East |
| D | North | TRUE | D | North |
| B | NOKA | FALSE | B | KALO |
| C | South-East | FALSE | C | MINA |
+--------+------------+-------+--------+------------+
I would like to match the two columns (Region, Mag) on the left with the two columns (Region, Mag) on the right, so in the end I would have my comparison column (which has a formula like =exact(string1,string2)) with TRUEs only.
I want to have a kind of button so that my two columns (Region, Mag) on the right of Comp could sort themselves.
I had this script, thanks to #JPV
function onOpen() {
SpreadsheetApp.getUi().createMenu('Sort').addItem('Sort Col D and E', 'sort').addToUi();
}
function sort() {
var ss = SpreadsheetApp.getActiveSheet();
var srange = ss.getRange('A2:B7').getValues();
var trange = ss.getRange('D2:E7');
var trangeVal = trange.getValues();
var returnarr = [];
for (var i = 0, ilen = trangeVal.length; i < ilen; i++) {
for (var j = 0, jlen = srange.length; j < jlen; j++) {
if (trangeVal[i][0] == srange[j][0] && trangeVal[i][1] == srange[j][1]) {
returnarr[j] = trangeVal[i];
}
}
}
trange.setValues(returnarr);
}
But seems not working and throwing an error like "Cannot convert Array to Object[][]"
Any help please!
Again thanks to #JPV
Ok. Maybe this script will help you ?
function onOpen() {
SpreadsheetApp.getUi().createMenu('Sort').addItem('Sort Col D and E', 'sort').addToUi();
}
function sort() {
var ss = SpreadsheetApp.getActiveSheet();
var srange = ss.getRange('A2:B7').getValues();
var trange = ss.getRange('D2:E7');
var trangeVal = trange.getValues();
var returnarr = [];
for (var i = 0, ilen = trangeVal.length; i < ilen; i++) {
for (var j = 0, jlen = srange.length; j < jlen; j++) {
if (trangeVal[i][0] == srange[j][0] && trangeVal[i][1] == srange[j][1]) {
returnarr[j] = trangeVal[i];
}
}
}
trange.setValues(returnarr);
}
Note: this will only work if the values in D&E are somewhere to be found in A&B. Also be aware of the fact that his will overwrite the values in D&E.
Test sheet here