Merge cells having same values bottom down - google-apps-script

I have a google sheet where i need to merge the cells if the values are repeating
The expected is to merge the cell from A1 to A4 and have a single value A in it.
I have tried with,
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var range = sheet.getActiveRange();
//var values = range.getValues();
var numCols = values[0].length;
values = [
["A", "B", "C", "D", "E"],
["A", "B", "S", "D", "E"],
["A", "D", "C", "D", "E"],
["A", "B", "C", "D", "K"],
["c", "B", "W", "D", "K"],
["A", "B", "C", "D", "E"],
]
for (var j = 0; j < numCols; j++) {
var placer = '';
for (var i = 0; i < values.length - 1; i++) {
if (values[i][j] == values[i + 1][j])
range.getCell(i + 2, j + 1).setValue('');
}
}

You want to merge cells vertically when the cells have the same values to the vertical direction.
You want to achieve the following situation.
Input
Output
If my understanding is correct, how about this modification? Please think of this as just one of several answers.
The flow of my sample script is as follows.
Transpose the values.
Start and end addresses for merging cells are retrieved from the transposed values.
Merge cells using the retrieved addresses.
Sample script:
// Retrieve values from the active sheet.
var sheet = SpreadsheetApp.getActiveSheet();
var values = sheet.getDataRange().getValues();
// Transpose values.
var res1 = values[0].map(function(_, i) {return values.map(function(e) {return e[i]})});
// Merge cells.
res1.forEach(function(col, i) {
var temp = {};
col.forEach(function(row, j) {
if (row === col[j + 1] && !(row in temp)) {
temp[row] = j;
} else if (row != col[j + 1] && row in temp) {
sheet.getRange(temp[row] + 1, i + 1, (j - temp[row]) + 1, 1).merge();
temp = {};
}
});
});
Note:
This is a simple sample script. So please modify this for your situation.
References:
merge()
forEach()
If I misunderstood your question and this was not the result you want, I apologize.
Edit:
This is the answer for the additional question.
Usage:
Manually select cells "A2:C6" on "Sheet1".
Run the script.
Script:
// Retrieve values from the active sheet.
var range = SpreadsheetApp.getSelection().getActiveRange();
var sheet = range.getSheet();
var values = range.getValues();
// Transpose values.
var res1 = values[0].map(function(_, i) {return values.map(function(e) {return e[i]})});
// Offset
var r = range.getRow() - 1;
var c = range.getColumn() - 1;
// Merge cells.
res1.forEach(function(col, i) {
var temp = {};
col.forEach(function(row, j) {
if (row === col[j + 1] && !(row in temp) && row != "") {
temp[row] = j;
} else if (row != col[j + 1] && row in temp) {
sheet.getRange(r + temp[row] + 1, c + i + 1, (j - temp[row]) + 1, 1).merge();
temp = {};
}
});
});
Result:
Before:
After:

Related

Why is this function not deleting many rows at once correctly?

This function works with a little bit of data, but not with hundreds of rows and I wonder if I'm missing some Spreadsheet.flush() or something of this nature.
const values = [["2022-12-31T06:00:00.000Z"],["2022-12-31T06:00:00.000Z"],["2022-12-31T06:00:00.000Z"],["2022-12-31T06:00:00.000Z"],["2022-12-31T06:00:00.000Z"],["2022-12-31T06:00:00.000Z"],["2022-12-31T06:00:00.000Z"],["2022-12-31T06:00:00.000Z"],["2022-12-31T06:00:00.000Z"],["2022-12-31T06:00:00.000Z"],["2022-12-31T06:00:00.000Z"],["2022-12-31T06:00:00.000Z"]];
function DeleteRows(sheetName, year) {
sheetName = 'Saved Budgets'//For tests
year = '2022' //For tests
var SS = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName); //Get Open Lines Sheet
var lastRow = SS.getLastRow();
var range = SS.getRange(2, 1, lastRow - 1, 1); //get range
range.sort({ column: 1, ascending: false }) // filter data descending
var firstRowToDelete = 0;
var numOfRows = 1; // starting row to be increment and become the number of rows
var values = range.getValues();//Got it for comparison
for (let a = 0; a < values.length; a++) {
let dt = new Date(values[a]).getFullYear();
if (dt == year) {
firstRowToDelete = parseInt(a);
numOfRows++
}
}
if (numOfRows != 1) {
numOfRows = numOfRows - 1 // minus 1 to get the last row
SS.deleteRows(firstRowToDelete, numOfRows);
}
range.sort({ column: 1, ascending: true }) // filter data again ascending
}
If you want to delete rows that the column "A" is year = '2022', firstRowToDelete = parseInt(a); is the last index of the rows that the column "A" is year = '2022'. And, numOfRows is the number of rows. In this case, I'm worried that all rows that the column "A" is year = '2022' cannot be deleted. And also, when the values are large, the rows for deleting might be over the bottom of the sheet, and/or range of range.sort({ column: 1, ascending: true }) might be over the bottom of the sheet. I thought that this might be the reason for your issue.
If you want to remove this issue, when your script is modified, how about the following modification?
From:
for (let a = 0; a < values.length; a++) {
let dt = new Date(values[a]).getFullYear();
if (dt == year) {
firstRowToDelete = parseInt(a);
numOfRows++
}
}
if (numOfRows != 1) {
numOfRows = numOfRows - 1 // minus 1 to get the last row
SS.deleteRows(firstRowToDelete, numOfRows);
}
range.sort({ column: 1, ascending: true }) // filter data again ascending
To:
for (let a = 0; a < values.length; a++) {
let dt = new Date(values[a]).getFullYear();
if (dt == year) {
if (firstRowToDelete == 0) firstRowToDelete = a + 2; // Modified
numOfRows++
}
}
if (numOfRows != 1) {
numOfRows = numOfRows - 1;
SS.deleteRows(firstRowToDelete, numOfRows);
}
SS.getRange(2, 1, SS.getLastRow() - 1, 1).sort({ column: 1, ascending: true }); // Modified
As another modification, how about the following modification?
From:
var values = range.getValues();//Got it for comparison
for (let a = 0; a < values.length; a++) {
let dt = new Date(values[a]).getFullYear();
if (dt == year) {
firstRowToDelete = parseInt(a);
numOfRows++
}
}
if (numOfRows != 1) {
numOfRows = numOfRows - 1 // minus 1 to get the last row
SS.deleteRows(firstRowToDelete, numOfRows);
}
range.sort({ column: 1, ascending: true }) // filter data again ascending
To:
var values = range.getDisplayValues();
var numOfRows = values.filter(([a]) => new Date(a).getFullYear() == year).length;
if (numOfRows > 0) {
var firstRowToDelete = values.findIndex(([a]) => new Date(a).getFullYear() == year);
SS.deleteRows(firstRowToDelete > -1 ? firstRowToDelete + 2 : firstRowToDelete, numOfRows);
}
SS.getRange(2, 1, SS.getLastRow() - 1, 1).sort({ column: 1, ascending: true });
Try this:
NOTE: This is all based on the assumption that the values global variable is actually a data in the spreadsheet, and you would want to remove all data with 2022.
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); //you can change this to specify a specific sheet
var range = ss.getRange(2,1,ss.getLastRow(), ss.getLastColumn());
var values = range.getValues();
var year = /2022/; // change this to filter other years
var newval = values.filter(x=>year.test(x) ? null : x);
console.log(newval); //to check if it populates the correct data during logging.
range.clearContent(); //clears the data based on the current range keeping the formatting.
var newrange = ss.getRange(2,1,newval.length, ss.getLastColumn()); //creates a new range based on the size of `newval`
newrange.setValues(newval);
}
Explanation:
var range = ss.getRange(2,1,ss.getLastRow(), ss.getLastColumn()); gets the current data on the spreadsheet, including the columns.
Using var values = range.getValues(); we get a 2D array structure of the data on the spreadsheet.
Using filter() and test() method on var newval = values.filter(x=>year.test(x) ? null : x); using a ternary operator to test whether an array element contains the year to filter out.
range.clearContent(); to delete the contents of the range.
var newrange = ss.getRange(2,1,newval.length, ss.getLastColumn()); creates a new range based on the new array.
newrange.setValues(newval); sets the new value on the spreadsheet
Screenshots:
NOTE: Multiple columns in the data are for testing to see dynamic deletion even if there is additional data on the columns.
Initial data:
After running the script:
Execution duration:
References:
https://developers.google.com/apps-script/reference/spreadsheet/range#clearContent()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test

How do I add the values from three arrays?

Is there an easy way to add the elements of arrays together? Let's say I have 3 arrays.
[15,22,35,40]
[10,20,12,20]
[11,24,14,22]
What I want to do is add up the totals so it would be:
[36,66,61,82]
and then overwrite [11,24,14,22] with those values.
Forgot to add the code I already have:
function combineItems(){
sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var dataRange = sheet.getDataRange();
var values = dataRange.getValues();
for (var i = 0; i < values.length; i++) {
var row = "";
for (var j = 0; j < values[i].length; j++) {
if (values[i][j] == "Product A") {
medData = sheet.getRange(i + 1, 7, 1, 4).getValues();
console.log(medData);
}
if (values[i][j] == "Product B") {
smData = sheet.getRange(i + 1, 7, 1, 4).getValues();
console.log(smData);
}
if (values[i][j] == "Product C") {
newData = sheet.getRange(i + 1, 7, 1, 4).getValues();
console.log(newData);
}
}
}
}
So I want to add up Product A, Product B, and Product C and then overwrite Product C with those new totals.
Solution:
function myFunction(data) {
return data[data.length-1].map((i, index) => i =
data.reduce((prev, curr) => prev + curr[index], 0)
)
}
Example:
const productA = [15,22,35,40]
const productB = [10,20,12,20]
const productC = [11,24,14,22]
const newData = myFunction([productA, productB, productC])
/**
Expected Output:
[36.0, 66.0, 61.0, 82.0]
**/
How It Works:
Taking the input of a multidimensional array, we alter the last row in place (.map()) by reducing (.reduce()) all values of the input data by index for each row.
(Reduce each column of the data into the last row.)
Suggested Reading:
Array.map()
Array.reduce()

Insert timestamp when value paste/edits in whole row in google sheets, only insert to blank cell

I'm trying to insert a timestamp into a cell in the "timestamp" column with the same row index as the edited or pasted value in the "status" column, only insert a timestamp in to an empty cell in "timestamp" column, while skipping the cell that already has a value in the "timestamp" column.
I'd also like to convert the date to a number, such as yymmddHHmmss*1000 + seri number column "No." Exp: If the timestamp is 22:01:20 14:08:05 and the sequence number in column "No." is 678, then value I want to insert into column "ID" is 2201201408050678.
I need to use the column header as a reference in this code to ensure that the function works correctly when the column index changes.
Issue: When changing many rows, this code simply repeats the original function for each selected cell. It gets the job done, but not too quickly.
How can I improve code speed when a multi-row update occurs?
No.
Timestamp
ID
Status
20
24:01:22 15:01:30
2201241501300020
Approved
17
Process
16
24:01:22 15:59:10
2201241559100016
Approved
16
function neworder2_onEdit(e) {
var sheet = e.range.getSheet();
if ((sheet.getSheetName() == 'RETAIL_ORDER') || (sheet.getSheetName() == 'HAMPER_ORDER') || (sheet.getSheetName() == 'SEA FOOD_ORDER') || (sheet.getSheetName() == 'GARDEN_ORDER'))
{
var col = e.range.columnStart;
var col_header = sheet.getRange(1,col).getValue();
if (col_header != 'Status') return;
var headers = sheet.getRange(1,1,1,sheet.getLastColumn()).getValues()[0];
var timestamp_col = headers.indexOf('Timestamp') + 1;
var num_col = headers.indexOf('No.') + 1;
var id_col = headers.indexOf('ID') + 1;
var row_start = e.range.rowStart;
var row_end = e.range.rowEnd;
if (sheet.getRange(row_start,col).getValue() != 'Approved') return;
var tz = SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone();
var timestamp = Utilities.formatDate(new Date(), tz, 'yy-MM-dd HH:mm:ss');
for (let row = row_start; row <= row_end; row++) {
var timestamp_cell = sheet.getRange(row, timestamp_col);
if (timestamp_cell.getValue() !== '') continue;
timestamp_cell.setValue(timestamp).setNumberFormat('yy:MM:dd HH:mm:ss');
var num = sheet.getRange(row,num_col).getValue().toString().padStart(4,'0');
var id = timestamp.replace(/\D/g,'') + num;
var id_cell = sheet.getRange(row,id_col);
id_cell.setValue(id);
}
}
}
I believe your goal is as follows.
Your script works fine. You want to reduce the process cost of the script.
In this case, how about the following modification?
Modified script:
function neworder2_onEdit(e) {
var sheet = e.range.getSheet();
if (['RETAIL_ORDER', 'HAMPER_ORDER', 'SEA FOOD_ORDER', 'GARDEN_ORDER'].includes(sheet.getSheetName())) { // Modified
var col = e.range.columnStart;
var col_header = sheet.getRange(1, col).getValue();
if (col_header != 'Status') return;
var row_start = e.range.rowStart;
var row_end = e.range.rowEnd;
// I modified below script.
var values = sheet.getRange(row_start, 1, row_end - row_start + 1, 4).getValues();
if (!values.some(r => r[3] == 'Approved')) return;
var tz = e.source.getSpreadsheetTimeZone();
var timestamp = Utilities.formatDate(new Date(), tz, 'yy-MM-dd HH:mm:ss');
var res = values.map(([a, b, c]) => (a == "" || b != "") ? [b, c] : [timestamp, timestamp.replace(/\D/g, '') + a.toString().padStart(4, '0')]);
sheet.getRange(row_start, 2, row_end - row_start + 1, 2).setValues(res);
sheet.getRange(row_start, 2, row_end - row_start + 1, 1).setNumberFormat('yy:MM:dd HH:mm:ss');
}
}
In this modification, after the array was created using the script in your for loop, the array was put to the sheet.
Reference:
map()
Added 1:
From your following replying,
When "status", "ID" or "timstamp" column index change, in case I want to insert column then our code not working. Can we use column header (status, timestamp, ID, No.) as pramameter for my cript? Can you give suggestion to do this?
In this case, how about the following sample script?
Sample script:
function onEdit(e) {
var sheet = e.range.getSheet();
if (['RETAIL_ORDER', 'HAMPER_ORDER', 'SEA FOOD_ORDER', 'GARDEN_ORDER'].includes(sheet.getSheetName())) { // Modified
var col = e.range.columnStart;
var header = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0].map(h => h.toLowerCase()); // Added
var obj = header.reduce((o, e, i) => (o[e] = i, o), {});
var col_header = header[col - 1]; // Modified
if (col_header != 'status') return;
var row_start = e.range.rowStart;
var row_end = e.range.rowEnd;
// I modified below script.
var values = sheet.getRange(row_start, 1, row_end - row_start + 1, header.length).getValues();
if (!values.some(r => r[obj["status"]] == 'Approved')) return;
var tz = e.source.getSpreadsheetTimeZone();
var timestamp = Utilities.formatDate(new Date(), tz, 'yy-MM-dd HH:mm:ss');
var res = values.map(r => {
if (!(r[obj["no."]] == "" || r[obj["timestamp"]] != "")) {
r[obj["timestamp"]] = timestamp;
r[obj["id"]] = timestamp.replace(/\D/g, '') + r[obj["no."]].toString().padStart(4, '0');
}
return r;
});
sheet.getRange(row_start, 1, row_end - row_start + 1, res[0].length).setValues(res);
sheet.getRange(row_start, [obj["timestamp"]] + 1, row_end - row_start + 1).setNumberFormat('yy:MM:dd HH:mm:ss');
}
}
In this script, from your question, it supposes that the header values are No.,Timestamp,ID,Status. Please be careful this.
Added 2:
From your following new issue,
It's working but there is some issue with my sheet. when scipt ran that paste value to all column, some of theme using arrayformula so all column use arrayformula will get error "#REF!" Can we just paste value to column timestame, id.
In this case, how about the following sample script?
Sample script:
function onEdit(e) {
var sheet = e.range.getSheet();
if (['RETAIL_ORDER', 'HAMPER_ORDER', 'SEA FOOD_ORDER', 'GARDEN_ORDER'].includes(sheet.getSheetName())) { // Modified
var col = e.range.columnStart;
var header = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0].map(h => h.toLowerCase()); // Added
var obj = header.reduce((o, e, i) => (o[e] = i, o), {});
var col_header = header[col - 1]; // Modified
if (col_header != 'status') return;
var row_start = e.range.rowStart;
var row_end = e.range.rowEnd;
// I modified below script.
var values = sheet.getRange(row_start, 1, row_end - row_start + 1, header.length).getValues();
if (!values.some(r => r[obj["status"]] == 'Approved')) return;
var tz = e.source.getSpreadsheetTimeZone();
var timestamp = Utilities.formatDate(new Date(), tz, 'yy-MM-dd HH:mm:ss');
var res = values.map(r => {
if (r[obj["no."]] != "" && r[obj["timestamp"]] == "" && r[obj["status"]] == "Approved") {
r[obj["timestamp"]] = timestamp;
r[obj["id"]] = timestamp.replace(/\D/g, '') + r[obj["no."]].toString().padStart(4, '0');
}
r.shift();
return r;
});
sheet.getRange(row_start, 2, row_end - row_start + 1, res[0].length).setValues(res);
sheet.getRange(row_start, [obj["timestamp"]] + 2, row_end - row_start + 1).setNumberFormat('yy:MM:dd HH:mm:ss');
}
}

Google Sheet script getValues and when find the given value, replace the value of the above row

How can I modify the script below to read the values in a column, but only read the values of the specified cells: L9, L10 ... L18, L19 .... L27, L28 .... (two by two jumping eight, to the end of the column L2000) and when find the value "1" in one of those cells, then replace the value that is 4 lines up with "FALSE"?
e.g. if cell L19 is found the number 1, then replace the value of cell L15 with the word "FALSE" ... and if cell L28 is found the number 1, then replace the value of cell L24 with the word "FALSE"
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("ABC");
var lastrow = ss.getLastRow();
var range = ss.getRange(9, 12, lastrow - 1, 1);
var data = range.getValues();
for (var i=0; i < data.length; i++) {
if (data[i][0] == "1") {
data[i][0] = "FALSE";
}
}
range.setValues(data);
Add another counter to loop: Try Modifying
From:
var range = ss.getRange(9, 12, lastrow - 1, 1);
var data = range.getValues();
for (var i=0; i < data.length; i++) {
if (data[i][0] == "1") {
data[i][0] = "FALSE";
}
}
To:
var range = ss.getRange(5, 12, lastrow+5, 1);
var data = range.getValues();
var m= 0;
for (var i=4; i < data.length; ) {
if (data[i][0] == "1") {
data[i-4][0] = "FALSE";
}
m++;
if (m == 2) {
m=0;
i = i + 8;
} else {
i++;
}
}
My take on what the loop should become:
var range = ss.getRange(9, 12, lastrow - 1, 1);
var data = range.getValues();
var i = 4,
// Subtract 1 here because we compare both i and i+1 in the same loop body
len = data.length - 1;
while (i < len) {
// Check the value of the first row in the subset.
if (data[i][0] == 1) {
data[i - 4][0] = "FALSE";
}
// Check the next row in the subset.
if (data[++i][0] == 1) {
data[i - 4][0] = "FALSE";
}
// Advance to the next subset.
i += 8;
}

Google Apps Script spreadsheet date manipulation

I have three scripts that are in a google docs spreadsheet. In this spreadsheet, in column H (or column 8), if I type an "x", the script changes it into that days date. After a few days, every date in column H has changed from a date to just a number. The numbers look like this: 40492, 40494, 40511. I am not sure what is causing this. Maybe it's something that is wrong in my script. I've pasted them below. Any ideas?
function onEdit(e) {
var colorA = "yellow";
var colorB = "#dddddd";
var colorC = "#dddddd";
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Purchase Orders");
var range = e.source.getActiveRange();
var sheetName = SpreadsheetApp.getActiveSheet().getName();
if (sheetName == "Purchase Orders") {
// 3 is column C
if (range.getColumn() == 3 && range.getValue() != "") {
sheet.insertRowAfter(range.getRow());
var r = range.getRow() + 1;
sheet.getRange("A" + r + ":H" + r).setBackgroundColor(colorC);
}
}
var col = e.source.getActiveRange().getColumn();
if(col == 3 || col == 8) {
var rows = sheet.getMaxRows();
//column C
var rangeC = sheet.getRange("C1:C"+rows);
var valuesC = rangeC.getValues();
//column H range
var rangeH = sheet.getRange("H1:H"+rows);
var colorH = rangeH.getBackgroundColors();
var valuesH = rangeH.getValues();
//iterate over each row in column C and H
//then change color
for (var row = 0; row < valuesC.length; row++) {
//check for columnC and column H
var hRow = colorH[row];
if (valuesC[row][0] != "" && valuesH[row][0] == "") {
hRow[0] = colorA;
} else if (valuesH[row][0] != "") {
hRow[0] = colorB;
}
}
sheet.getRange("H1:H" + rows).setBackgroundColors(colorH);
}
}
And this one
function onEdit(e) {
var ss = e.source.getActiveSheet();
var r = e.source.getActiveRange();
// 1 is A, 2 is B, ... 8 is H
if (r.getColumn() == 8 && r.getValue() == "x") {
r.setValue(Utilities.formatDate(new Date(), "MST", "yyyy-MM-dd"));
}
}
And this last one
ss = SpreadsheetApp.getActiveSpreadsheet();
function onOpen() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var menuEntries = [ {name: "New PO", functionName: "NewPO"}];
ss.addMenu("New PO", menuEntries);
}
function NewPO() {
SpreadsheetApp.getActiveSheet().insertRowsBefore(1,6);
// Adjust this range accordingly, these are the cells that will be
// copied. Format is getRange(startRow, startCol, numRows, numCols)
ss.getSheetByName("PO Form").getRange(1, 1, 6, 8)
.copyTo(SpreadsheetApp.getActiveSheet().getRange(1, 1, 6, 8));
}
In OnEdit, you probably want to set the format for that cell as well. setNumberFormat(numberFormat) appears to be the function you are after.
http://code.google.com/googleapps/appsscript/class_range.html#setNumberFormat