sorting columns independently at array level - google-apps-script

I have a spreadsheet with several columns that I need to sort individually.
I wrote the script below that works but is a bit slow since it handles each column in turn with getValues() and setValues().
I'd like to find a way to do the whole sorting at an array level for more efficiency but I don't know how... any suggestion ?
Here is the relevant part of the code I use now :
...
sh3.getRange(1,1,1,newData[0].length).setFontWeight('bold');// newData is an array corresponding to the whole sheet
for(col=1;col<newData[0].length;++col){
var val = sh3.getRange(2,col,getLastRowInCol(col),1).getValues().sort();// each column have a different height
sh3.getRange(2,col,getLastRowInCol(col),1).setValues(val)
}
}
function getLastRowInCol(col){
var values = sh3.getRange(2,col,sh3.getLastRow(),1).getValues();// skip header and find last non empty row in column
for(n=0;n<values.length;++n){
if(values[n]==''){break}
}
return n
}
Note : I know there is a Library by Romain Vialard that does the job (sorting columns in 2D arrays) but I'm interrested on how to do it 'manually' for personal JS skills improvement ;-) and also I need to sort every column independently without needing to update the sheet for every column.

How about:
function sortColumns() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2;
var startCol = 1;
var dataRange = sheet.getRange(startRow, startCol, sheet.getLastRow() - startRow + 1, sheet.getLastColumn() - startCol + 1);
var data = dataRange.getValues();
// transpose data so each column item will be listed in an single array
// for each column so that it can be sorted with array.sort()
var rowToCol = [];
for (var i = 0; i < data[0].length; i++) {
rowToCol.push([]);
for (var j = 0; j < data.length; j++) {
// replace empty string with undefined as undefined sorts last
rowToCol[i].push(data[j][i]==""?undefined:data[j][i]);
}
rowToCol[i].sort();
// default sort, as above, is alphabetic ascending. For other methods
// search for Javascript array sort functions
}
// transpose sorted items back to their original shape
var result = [];
for (var i = 0; i < rowToCol[0].length; i++) {
result.push([]);
for (var j = 0; j < rowToCol.length; j++) {
result[i].push(rowToCol[j][i]==undefined?"":rowToCol[j][i]);
}
}
dataRange.setValues(result);
};
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [{
name : "Sort Columns",
functionName : "sortColumns"
}];
sheet.addMenu("Script Center Menu", entries);
};

Related

Trying to copy cell value to different column(s) dependent on variable value

i'm trying to transpose an array to a column dependent on a variable value.
Essentially what i'm trying to do is build a script which will transpose an input sheet, and depending on the language used in the specific input sheet row, transpose that rows values to a different column on an output.
i.e. depending on column value (C), values in columns A,B transpose to Column I,J,K depending on column value C.
Google sheets link: https://docs.google.com/spreadsheets/d/1wuBfK4PKg79vCGGhsBPmHPT7hjfYNdR0srvX9nj0Ajs/edit?usp=sharing
I've already found a simple copyto script which works if it's one array with same language, but I need it to offset based on language.
Any help will be appreciated.
example code:
//missing the offset
function transpose() {
var sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange("A2:C").copyTo(sheet.getRange("F2"),
SpreadsheetApp.CopyPasteType.PASTE_VALUES,true);
}
Proposed Script
function createReport() {
// Initilaize Sheets, Ranges, Values
let file = SpreadsheetApp.getActive();
let sheet1 = file.getSheetByName("Blad1");
let range1 = sheet1.getDataRange();
let values1 = range1.getValues();
let sheet2 = file.getSheetByName("Blad2");
let range2 = sheet2.getDataRange();
let values2 = range2.getValues();
// Deal with headers
let langs = values2[0];
values1.shift(); // to remove headers
// Creating array of sub arrays with info to paste into report
// In this format:
// [[Column to paste in, Input 1, Input 2, Input 3]]
let output = [];
values1.forEach((row, i) => {
let outputRow = [];
let whichCol = langs.findIndex((i) => i == row[3]) + 1;
outputRow.push(whichCol);
for (let i = 0; i < 3; i++) {
outputRow.push(row[i]);
}
output.push(outputRow);
});
// With output array, pasting into report
output.forEach((entry) => {
let col = entry.shift();
// Find where the next free slot is in column
let occupiedRange = sheet2
.getRange(1, col)
.getDataRegion(SpreadsheetApp.Dimension.ROWS)
let height = occupiedRange.getHeight();
// Transposing array
set = entry.map((val) => [val]);
// Inserting Values to Report
let outRange = sheet2.getRange(height + 1, col, 3, 1);
outRange.setValues(set);
});
}
Source Data in Blad1
Destination Template and script in action in Blad2
Explanation
You'll notice its quite a bit longer than your script! What you are trying to do is deceptively complex, which is why I hesitated to answer fully as this script is so far removed from what you initially posted that it almost seemed like it was a "give me the code" question. Though you are new on the site and I had already written out most of the code, so what the hell. In future please try to include more info in your original question, your attempts and research. I have tried to keep it an concise as possible, but there may be certain syntax that you haven't come across, like forEach and map.
The script first gets the data with getValues that returns 2D arrays of the values.
I take out the headers on the source data, and use the headers on the target data to find the column index where the source data will end up. So ENG is index 1, and X index 2 etc.
For each row in the source data it transforms it into an intermediary array (which is not necessary, but I think its clearer to understand each step). The intermediary array is composed of sub arrays representing each "set". Each sub array has this format [Column_Index, Input1, Input2, Input3].
Once this has been build, each of those sub arrays can be gone through to insert them into the output sheet, which I have called the "Report".
Within this process is the need to get the first unoccupied row of the target column. So if ENG already has 3 sets that have been filled in, the script needs to know where the next set starts. It does this by using getDataRegion(SpreadsheetApp.Dimension.ROWS) then getHeight() + 1 to find the starting row for the set to be inserted.
Also within this final process is the need to transform the array from this format:
[1,2,3]
Which Apps Script understands as a row, to a column, which would be this:
[[1],[2],[3]]
which was done with map.
I encourage you to use Logger.log to log a bunch of the values and inspect the output so that you can understand the script and adapt it to your needs. I have tried to name everything in a "friendly" manner.
References
Map
forEach
Range object
Try this code:
function Sort()
{
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sh = ss.getSheetByName("Blad1")
var input = sh.getRange(1, 1, 4, 4).getValues();
var output = sh.getRange("H1:K1").getValues();
var index = [0]; var nInput = []
for(var j = 1; j < output[0].length; j++)
{
for(var i = 1; i < input.length; i++)
if(output[0][j] == input[i][3]) index.push(i)
}
for(var i = 0; i < input.length; i++) nInput.push(input[index[i]])
var nData = [];
for(var i = 1; i < nInput.length; i++)
{
for(var j = 0; j < nInput[0].length-1; j++)
{
if(nData[j] == null) nData[j] = []
nData[j].push(nInput[i][j])
}
}
sh.getRange(2, 9, nData.length, nData[0].length).setValues(nData);
}
Try this code:
function Sort()
{
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sh = ss.getSheetByName("Blad1")
var input = sh.getRange(1, 1, 7, 4).getValues();
var output = sh.getRange("H1:K1").getValues();
var index = [0]; var nInput = []
for(var j = 1; j < output[0].length; j++)
{
nInput[j-1] = []
for(var i = 1; i < input.length; i++)
if(output[0][j] == input[i][3])
for(var k = 0; k < input[0].length-1; k++) nInput[j-1].push(input[i][k])
}
var nData = [];
for(var i = 0; i < nInput.length; i++)
{
for(var j = 0; j < nInput[0].length; j++)
{
if(nData[j] == null) nData[j] = []
if(nInput[i][j]) nData[j].push(nInput[i][j])
else nData[j].push("")
}
}
sh.getRange(2, 9, nData.length, nData[0].length).setValues(nData)
}
This code will work only if you have same number of inputs (3 in this case) for each language.

Clear content insted of deleting row

I have a range with data where I need a script to look for duplicates in the first column, and clear contents of the hole row in that range.
From this
To this
I've found this script, but it deletes the whole row. I need it to only clear content.
function ClearDuplicates() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("03");
var rows = sheet.getLastRow();
var firstColumn = sheet.getRange("A:A").getValues();
for (var i = rows; i >= 2; i--) {
if (firstColumn[i-1][0] == firstColumn[i-2][0]) {
sheet.deleteRow(i);
}
}
}
You need to retrieve the array data from column A and iterate through it comparing with the same array. When a match happens, apply clearContent() function [1] to clear the contents on that range (First get the range you want to clear in that row).
function ClearDuplicates() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var rows = sheet.getLastRow();
var firstColumnValues = sheet.getRange("A1:A" + rows).getValues();
for (var i = 0; i < firstColumnValues.length; i++) {
var value = firstColumnValues[i][0];
for (var j = 0; j < firstColumnValues.length; j++) {
var comparisonValue = firstColumnValues[j][0];
if ((value == comparisonValue) && (j>i)) {
//You need to change the number of columns to which you need to clear the contents, in this case is 10
sheet.getRange(j+1, 1, 1, 10).clearContent();
}
}
}
}
[1] https://developers.google.com/apps-script/reference/spreadsheet/range#clearcontent
You can just grab the range and clear it instead as seen below:
sheet.getRange("R"+i+"C").clear();

How can I optimize my code using batch operations?

I want to perform a basic multiplication on a range of values in my spreadsheet, and then divide those values by a range of values from a column, note that my range is 8 columns long and my division range is one column long.
I have this code:
function multiply() {
var doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty("1iXQxyL3URe1X1FgbZ76mEFAxLnxegyDzXOMF6WQ5Yqs"));
var sheet = doc.getSheetByName("json");
var sheet2 = doc.getSheetByName("tabla de frecuencias");
var sheet3 = doc.getSheetByName("Template");
var range = sheet2.getDataRange();
var numRows = range.getNumRows();
var numCols = range.getNumColumns()-1; //This will get the division values which are located in the last column
var targ = sheet2.getLastColumn();
for (var i = 2; i <= numRows; i++) {
for (var j = 2; j <= numCols; j++) {
var A = range.getCell(i,j).getValue();
var value = A;
for (var v = 1; v <= numRows; v++) {
var T = range.getCell(v,targ).getValue();
range.getCell(i,j).setValue(value*100/T);
}
}
}
}
It's very slow, it reads and writes on each cell from a sheet where I have numeric values ready to be multiplied by 100 and divided by a value located in a single column, this value is different for each row.
My script gets the job done extremly slowly, batch operations appear promising, if that's not the best solution, I will accept any other alternate solution regardless of the question title.
I think this is what your looking for.
function multiply()
{
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName("Sheet60");
var rg=sh.getDataRange();
var dataA=rg.getValues();
for(var i=1;i<dataA.length;i++)
{
for(var j=1;j<dataA[0].length-1;j++)
{
var value=dataA[i][j];
for (var v=1;v<dataA.length;v++)
{
var T=dataA[v][dataA[0].length-1];//the value in the last column on this row
Logger.log('v=%s dataA[%s][%s]=%s',v,i,j,dataA[i][j]);
dataA[i][j]=value * 100 / T;//This seems wrong because it puts a different value into dataA[i][j] which doesn't change inside this inner loop and so only the last value remains in dataA[i][j]
}
}
}
rg.setValues(dataA);
}
Try to minimize the use of getValue and replace with one getValues to get all values in two dimensional array. Then setValues all at one time at the end of the loop.
Okay made another change. Getting more reasonable results.
I'm beginning to think that you may not actually want the v loop at all. Take a look at this one and look at the Logger. We just write the data once.
function percentages()
{
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName("Sheet60");
var rg=sh.getDataRange();
var dataA=rg.getValues();
for(var i=1;i<dataA.length;i++)
{
for(var j=1;j<dataA[0].length-1;j++)
{
var value=dataA[i][j];
var T=dataA[i][dataA[0].length-1];
dataA[i][j]=value * 100 / T;
Logger.log('dataA[%s][%s]=%s',i,j,dataA[i][j]);
}
}
rg.setValues(dataA);
}
This is the output for this version.

Google script - Exceeded maximum execution time , help optimize

google script spreadsheet
Novice
I try to create a matrix , if the array is a small database everything works fine, of course if it exceeds 800 lines and more rests on the error "You have exceeded the maximum allowed run time ." Not effectively create a matrix :
var s = SpreadsheetApp.getActiveSheet(); //List
var toAddArray = []; //Greate Arr
for (i = 1; i <= s.getLastRow()+1; ++i){ //Start getting Value
var numbr = s.getRange(i,4); //detect range
var Valus = numbr.getValues().toString(); //get value
//filter value
var newznach = Valus.replace(/\-/g, "").replace(/[0-9][0-9][0-9][0-9][0-9][a-zA-Zа-яА-Я][a-zA-Zа-яА-Я]/g, "").replace(/[a-zA-Zа-яА-Я][a-zA-Zа-яА-Я]/g, "");
toAddArray.push([i.toFixed(0),Valus,newznach]); //add to array 0- Row numb, 1- Value, 2- "filtered" value
}
toAddArray =
{
Row, Value, NewValue - filtered
Row, Value, NewValue - filtered
Row, Value, NewValue - filtered
...
}
Can I somehow get an array of the same the other way ( faster, easier ) ?
You're doing a call to getValues every row, that eats a lot of performance.
It is better to do one big call to have all the data and then go through it sequentially.
var s = SpreadsheetApp.getActiveSheet();
var data = s.getRange(1,4, s.getLastRow()).getValues();
var toAddArray = data.map(function(row, i) {
var Valus = row[0].toString();
var newznach = Valus.
replace(/\-/g, "").
replace(/[0-9][0-9][0-9][0-9][0-9][a-zA-Zа-яА-Я][a-zA-Zа-яА-Я]/g, "").
replace(/[a-zA-Zа-яА-Я][a-zA-Zа-яА-Я]/g, "");
return [i.toFixed(0), Valus, newznach];
});
this code:
var Valus = numbr.getValues().toString();
slows you down because you read data from the sheet in a loop.
Try reading data once into array and then work with it:
var data = s.getDataRange().getValues();
And then work with data, in a loop. This sample code log each cell in active sheet:
function logEachCell() {
var s = SpreadsheetApp.getActiveSheet();
var data = s.getDataRange().getValues();
// loop each cell
var row = [];
for (var i = 0; i < data.length; i++) {
row = data[i];
for (var j = 0; j < row.length; j++) {
Logger.log(row[j])
}
}
}

Google Apps Script - Find Row number based on a cell value

I'm looking for some assistance on find the row number for a cell that contains a specific value.
The spreadsheet has lots of data across multiple rows & columns. I'm looping over the .getDataRange and can located the cell containing the value I'm looking for. Once found I'd like to know the row that his cell is in, so that I can further grab additional cell values since I know exactly how many rows down and/or columns over the additional information is from the found cell.
Here is a bit of the code for finding the cell containing a specific string.
function findCell() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
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] == "User") {
row = values[i][j+1];
Logger.log(row);
}
}
}
}
When outputting Logger.log(row) it gives me the values I'm looking for. I want to determine what row each value is in, so I can then go down X number of rows and over X columns to get the contents of other cells.
I don't know much about google apps but it looks like [i] is your row number in this circumstance.
So if you did something like:
function findCell() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
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] == "User") {
row = values[i][j+1];
Logger.log(row);
Logger.log(i); // This is your row number
}
}
}
}
This method finds the row number in a particular column for a given value:
function rowOf(containingValue, columnToLookInIndex, sheetIndex) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets();
var dataRange = sheet[sheetIndex].getDataRange();
var values = dataRange.getValues();
var outRow;
for (var i = 0; i < values.length; i++)
{
if (values[i][columnToLookInIndex] == containingValue)
{
outRow = i+1;
break;
}
}
return outRow;
}
But if you do it like that, it won't refresh unless you change any of the parameters, that's because a custom function should be deterministic, that is, for the same parameters, it should always give the same result.
So instead, it's better to do it this way:
function rowOf(containingValue, range) {
var outRow = null;
if(range.constructor == Array)
{
for (var i = 0; i < range.length; i++)
{
if(range[i].constructor == Array && range[i].length > 0)
{
if (range[i][0] == containingValue)
{
outRow = i+1;
break;
}
}
}
}
return outRow;
}
In this case, you need to pass the full column range your looking for like so:
rowOf("MyTextToLookFor", 'MySheetToLookIn'!A1:A)
Where you would replace A by the colum of your choice, and MySheetToLookIn by your sheet's name and MyTextToLookFor by the text you are looking for.
This will allow it to refresh on adding rows and removing rows.
Since 2018 this can be done without a loop using flat().
function findRow(searchVal) {
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getDataRange().getValues();
var columnCount = sheet.getDataRange().getLastColumn();
var i = data.flat().indexOf(searchVal);
var columnIndex = i % columnCount
var rowIndex = ((i - columnIndex) / columnCount);
Logger.log({columnIndex, rowIndex }); // zero based row and column indexes of searchVal
return i >= 0 ? rowIndex + 1 : "searchVal not found";
}
ES6 gave us a one liner for this (but expanded for full detail).
function findRow() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss. getActiveSheet(); // OR GET DESIRED SHEET WITH .getSheetByName()
const dataRange = sheet.getDataRange();
const values = dataRange.getValues();
const columnIndex = 3 // INDEX OF COLUMN FOR COMPARISON CELL
const matchText = "User"
const index = values.findIndex(row => row[columnIndex] === matchText)
const rowNumber = index + 1
return rowNumber
}
This can be done with the Text Finder:
function findRow(searchValue){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var thisSheet = ss.getActiveSheet();
var tf = thisSheet.createTextFinder(searchValue)
var thisRow = tf.findNext().getRow()
return thisRow
}
Discovered through this post: Google App Scripts find text in spreadsheet and return location index
Implementing: https://developers.google.com/apps-script/reference/spreadsheet/text-finder
To find a row in a Google Sheets sheet based on the value of a specific column (in this case, the invoice_id column) and edit another column (in this case, the send column) in that row, you can use the following script:
// Replace "Sheet1" with the name of your sheet
var sheet = SpreadsheetApp.getActive().getSheetByName("Sheet1");
// Replace "A" with the column letter for the invoice_id column
// and "B" with the column letter for the send column
var invoiceIdColumn = "A";
var sendColumn = "B";
// Replace "12345" with the invoice_id you want to search for
var invoiceIdToSearch = "12345";
// Find the row number of the row with the matching invoice_id
var data = sheet.getDataRange().getValues();
var row = data.findIndex(row => row[invoiceIdColumn - 1] == invoiceIdToSearch) + 1;
// If a row with the matching invoice_id was found
if (row) {
// Set the value of the send column in that row to "true"
sheet.getRange(row, sendColumn).setValue(true);
}
This script will search through the entire sheet for a row with an invoice_id value that matches the invoiceIdToSearch variable, and then set the value of the send column in that row to true.
Note that this script assumes that the invoice_id column is the first column in the sheet (column A) and the send column is the second column (column B). If your columns are in different positions, you will need to adjust the invoiceIdColumn and sendColumn variables accordingly.