I am trying to use a google sheet to rank a list of elements. This list is continually updated, so it can be troublesome to update the list if i already have hundreds of elements ranked and need to rank 10 new ones. Rather than having to re-rank some of the previously ranked elements every time (whether manually or using formulas), i thought it easier to write a macro that would re-rank for me.
1 - element A
2 - element B
3 - element C
new element: element D
For instance if i wanted element D to be ranked 2nd, i would need to change element B to 3 and element C to 4. This is tedious when doing hundreds of elements.
Here is my code so far but I get stuck with the getRange lines. Rankings are in column A.
function RankElements() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var r = s.getActiveCell();
var v1 = r.getValue();
var v2 = v1 + 1
var v3 = v2 + 1
var lastRow = s.getLastRow();
s.getRange(1,v2).setValue(v2);
s.getRange(1,v3).autoFill(s.getRange(1,v3+":"+1,lastRow), SpreadsheetApp.AutoFillSeries.DEFAULT_SERIES);
s.getRange(1,v3+":"+1,lastRow).copyTo(s.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
s.getFilter().sort(1, true);
};
You can do the following:
Iterate through all values in column A.
For each value, check if (1) ranking is equal or below the new one, and (2) it's not the element that is being added.
If both these conditions are met, add 1 to the current ranking.
It could be something like this:
function RankElements() {
const sheet = SpreadsheetApp.getActiveSheet();
const cell = sheet.getActiveCell();
const row = cell.getRow();
const newRanking = sheet.getActiveCell().getValue();
const firstRow = 2;
const columnA = sheet.getRange(firstRow, 1, sheet.getLastRow() - 1).getValues()
.map(row => row[0]); // Retrieve column A values
for (let i = 0; i < columnA.length; i++) { // Iterate through column A values
if (columnA[i] >= newRanking && (i + firstRow) != row) {
sheet.getRange(firstRow + i, 1).setValue(columnA[i] + 1); // Add 1 to ranking
}
}
sheet.getFilter().sort(1, true);
};
Related
Currently, my script is logging values in E based on the position of the last input in columns A,B. Is there a way to prevent these gaps?
var sss = SpreadsheetApp.openById('sampleID');
var ss = sss.getSheetByName('Forecast data');
var range = ss.getRange('B126');
const now = new Date();
const data = range.getValues().map(row => row.concat(now));
var tss = SpreadsheetApp.openById('sampleID2');
var ts = tss.getSheetByName('Archived Data');
ts.getRange(ts.getLastRow()+1, 5,1,2).setValues(data);
}
Try something like this:
ts.getRange(getLastRow_(ts, 5) + 1, 5, 1, 2).setValues(data);
Here's a copy of the getLastRow_() function:
/**
* Gets the position of the last row that has visible content in a column of the sheet.
* When column is undefined, returns the last row that has visible content in any column.
*
* #param {Sheet} sheet A sheet in a spreadsheet.
* #param {Number} columnNumber Optional. The 1-indexed position of a column in the sheet.
* #return {Number} The 1-indexed row number of the last row that has visible content.
*/
function getLastRow_(sheet, columnNumber) {
// version 1.5, written by --Hyde, 4 April 2021
const values = (
columnNumber
? sheet.getRange(1, columnNumber, sheet.getLastRow() || 1, 1)
: sheet.getDataRange()
).getDisplayValues();
let row = values.length - 1;
while (row && !values[row].join('')) row--;
return row + 1;
}
An alternative way to find it is via filter().
Code:
// Sample data to be iserted
data = [[2.4, '5/5/2021']]
var tss = SpreadsheetApp.openById(sampleID2);
var ts = tss.getSheetByName('Archived Data');
// get values on column E and filter the cells with values and get their length
var column = ts.getRange("E1:E").getValues();
var lastRow = column.filter(String).length;
ts.getRange(lastRow + 1, 5, 1, 2).setValues(data);
Sample data:
Output:
Note:
This approach is good when column has no blank cells in between. When you skip a cell, it will not calculate the lastRow properly and might overwrite data. But as long as you do not have gaps in your column, then this will be good.
Resource:
Determining the last row in a single column
The Google Sheets API seems vague and I'm probably just too tired.
function onEdit(e) {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var positives = sheet.getRange("D3:AG3");
var negatives = sheet.getRange("C4:C33");
for (i=0;i<positives.getLastColumn();i++) {
var j = positives[i]*-1;
negatives[i].setValue(j);
}
}
I'm sure I'm doing eight things wrong but if someone is more familiar with Google Sheets, please throw a brick at me.
First, positives is a ranges, and you need to use getValues() to get an array that you can manipulate.
Second, it's not recommended to use Sheets API methods inside loops, the best practice is to manipulate arrays in loops and then use single get and set values API to read / write to a range.
Sample Code:
function onEdit(e) {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var positives = sheet.getRange("D3:AG3").getValues();
var negatives = sheet.getRange("C4:C33");
var result = [];
for (i = 0; i < positives[0].length; i++) {
result.push([positives[0][i] * -1]);
}
negatives.setValues(result);
}
Sample Output: (I only put values in three rows)
Reference:
push()
Avoid using onEdit for these kind of changes as it will be resource intensive. You are changing all the values of the column into negative of the row EVERY TIME you edit the sheet (Unless that should be the case)
If you really want to use onEdit, be sure to limit it only when the specific range is edited.
Code:
function onEdit(e) {
const row = e.range.getRow();
const column = e.range.getColumn();
// if edited range is within D3:AG3
if(row == 3 && column >= 4 && column <= 33) {
// write to the corresponding row (invert col and row)
e.source.getActiveSheet().getRange(column, row).setValue(e.value * -1);
}
}
Note:
Behaviour of the onEdit function is that when you edit the range D3:AG3, it will negate its value and write into its corresponding destination, one by one.
If you edit D3, it will assign that negative value into C4, nothing more.
If you edit outside the positive range, it will not do anything.
Another approach is to copy your positive row into negative column by transforming your data structure into the destination by bulk.
Code:
function rowToColumn() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var pRange = sheet.getRange("D3:AG3");
var pValues = pRange.getValues();
// pValues is a 2D array now
// row range values = [[1, 2, 3, ...]
var negatives = sheet.getRange("C4:C33");
// column range values = [[1], [2], [3], ...]
// since structure of row is different than column
// one thing we can do is convert the row into column structure
// and multiply each element with -1, then assign to negatives
pValues = pValues.map(function(item) {
item = item.map(function(col) {
return [col * -1];
});
return item;
})[0];
// set values into the negatives range
negatives.setValues(pValues);
}
Note:
Behaviour of the rowToColumn function is that it transfers all the values of the row range and then put it into negatives range all at once.
Blank cells will yield 0 by default, add a condition on return [col * -1]; if you want blank cells to return other values instead.
Output:
I have been using the following script to move "Finished" columns from one sheet to another:
function onEdit(event) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = event.source.getActiveSheet();
var r = event.source.getActiveRange();
if(s.getName() == "Sheet1" && r.getColumn() == 15 && r.getValue() ==
"Finished") {
var row = r.getRow();
var numColumns = s.getLastColumn();
var targetSheet = ss.getSheetByName("Finished");
var target = targetSheet.getRange(targetSheet.getLastRow() + 1, 1);
s.getRange(row, 1, 1, numColumns).moveTo(target);
s.deleteRow(row);
}
}
Im trying to figure out how to get the script to run even if the sheet has not been opened or edited. Im just not sure how to go about changing it to use a time trigger every minute or so.
There are two aspects to the change to a timed trigger from onEdit.
The first concerns revisions to the code, the second is the trigger details.
Code
The code can't be re-used because the timed trigger doesn't provide the same event details as OnEdit.
In addition, it is possible that several rows might be tagged "Finished" between each trigger event, and the code needs to respond to them all. lastly, each "finished" row can't be deleted as it is found, because this affects the row number of all remaining rows in the column.
The following code would do the job:
Most of it will be familiar to the questioner. The main except is to keep a record of each row number that is moved to "Finished". This is done by pushing the row number onto an array. Then after all data has been examined and moved, there is a small loop that takes the row numbers recorded in the array and deletes the relevant row. The loop works from the highest row number to the lowest; this is so that the deletion of a row does not affect the row number of any remaining rows to be deleted.
function so_53305432() {
// set up the spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
// identify source and target sheets
var sourceSheet = ss.getSheetByName("Sheet1");
var targetSheet = ss.getSheetByName("Finished");
// get some variables to use as ranges
var sourcelastRow = sourceSheet.getLastRow();
var numColumns = sourceSheet.getLastColumn();
var targetLastRow = targetSheet.getLastRow();
// get data from the Source sheet
var sourceData = sourceSheet.getRange(1, 1, sourcelastRow, numColumns).getValues();
// set up some variables
var finishedRows = [];
var i = 0;
var x = 0;
var temp = 0;
// loop through column 15 (O) checking for value = "Finished"
for (i = 0; i < sourcelastRow; i++) {
// If value = Finished
if (sourceData[i][14] == "Finished") {
// define the target range and move the source row
var targetLastRow = targetSheet.getLastRow();
var target = targetSheet.getRange(targetLastRow + 1, 1);
sourceSheet.getRange(+i + 1, 1, 1, numColumns).moveTo(target);
// keep track of the source row number.
finishedRows.push(i);
}
}
// set up variables for loop though the rows to be deleted
var finishedLength = finishedRows.length;
var startcount = finishedLength - 1
// loop throught the array to delete rows; start with the highest row# first
for (x = startcount; x > -1; x--) {
// get the row number for the script
temp = +finishedRows[x] + 1;
// delete the row
sourceSheet.deleteRow(temp);
}
}
Trigger
The trigger needs to be revised. To do this:
1) Open the script editor, select Current Project Triggers. OnEdit should appear as an existing trigger with an event type of OnEdit.
2) Change "Choose which function to run" to the new function,
3) Change "Select Event Source" from Spreadsheet to "Time Driven".
4) Select "Type of time based trigger" = "Minutes Timer".
5) Select "Select Minute Interval" = , and select a time period and interval.
6) Save the trigger, and then close the Trigger tab
If "Every Minute" is found to be too often, then the Questioner could try "Every 5 minutes".
I'm not sure if this is even possible, and to be quite honest, I haven't tried many things because I wasn't sure where to even start. I'm using the Script Editor from Google Sheets, btw. I know there are SpreadsheetApp.getRange() and another to get the values or something like that. But what I want is a bit specific.
Is there a way to grab all the cell data in a given row and put it into an array? The rows will vary in size, that's why I can't do an exact range.
So for example, if I were to have rows have these values:
abc | 123 | 987 | efg
blah| cat | 654
I want to be able to grab those values and place them into an array like ["abc", "123", "987, "efg"]. And then if I run the function on the next row, it'd be ["blah", "cat", "654"].
Actually, it can be placed into any data type as long as there's a delimiter I'd be able to use.
Thank you in advance!
This is easier to achieve without a script, with the formula =filter(1:1, len(1:1)) returning all values in nonempty cells in row 1, etc.
From a script, you can do something like this:
function flat_nonempty() {
var range = SpreadsheetApp.getActiveSheet().getRange("A:A"); // range here
var values = range.getValues();
var flat = values.reduce(function(acc, row) {
return acc.concat(row.filter(function(x) {
return x != "";
}));
}, []);
Logger.log(flat); // flat list of values, no blanks
}
The range here can have one row or multiple rows.
Is this useful for you?
When there are abc | 123 | 987 | efg, blah| cat | 654 and abc | | 987 | efg at row 1, row 2 and row 3, myFunction(1), myFunction(2) and myFunction(3) return [abc, 123.0, 987.0, efg], [blah, cat, 654.0] and [abc, , 987.0, efg].
function myFunction(row){
var ss = SpreadsheetApp.getActiveSheet();
var values = ss.getRange(row, 1, 1, ss.getLastColumn()).getValues();
var c = 0;
for (var c = values[0].length - 1; c >= 0; c--){
if (values[0][c] != "") break;
}
values[0].splice(c + 1, values[0].length - c - 1);
return values[0];
}
Adapted from https://developers.google.com/apps-script/reference/spreadsheet/sheet#getDataRange()
You can loop and create your array directly if you do want an array. Here I am illustrating creating for all the rows, but if you know the row you want, you could do without the outer loop (and just set i to the desired row).
function rowArr() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
// This represents ALL the rows
var range = sheet.getDataRange();
var values = range.getValues();
for (var i = 0; i < values.length; i++) {
var row = [];
for (var j = 0; j < values[i].length; j++) {
if (values[i][j]) {
row.push(values[i][j]);
}
}
Logger.log(row);
}
}
I have an Input column with a sequence of two different letters. As result I want to get something like on the picture. This formulas I will use with ARRAYFORMULA to get unlimited count of rows. To get BLOCK № I was trying to use =COUNTIFS($B$2:B2,"N") but it works only if I copy the formula manually down the column, but if I do:
=ARRAYFORMULA(COUNTIFS(($B$2):(B2:B),"N"))
It doesn't work.
How can I replicate the behavior of this function without needed to manually copy it?
I'd recommend writing a script to fill the Block Nos.
I'll assume the topmost letter begins at cell input!A4 and you want the Block Nos from cell input!C5 and below. Go to the menu bar of the spreadsheet and select Script Editor. Then write the following scripts:
//the main function
function writeBlocks() {
var sheet = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('input');
var numRows = sheet.getLastRow();
var startRow = 4;
var inputCol = 1;
var outputCol = 3;
var block = 0;
//clear old Block Nos
sheet.getRange(startRow, outputCol, numRows - 3, 1)
.clearContent();
//recalculate LastRow in case there are fewer new inputs than old outputs
numRows = sheet.getLastRow();
//get input data
var input = sheet.getRange(startRow, inputCol, numRows - 3, 1)
.getValues;
//write output data
for (var i = 0; i < input.length; i++) {
block += input[i] == "N" ? 1 : 0;
sheet.getRange(startRow + i, outputCol)
.setValue(block);
}
}
//create new menu
function onOpen() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var menuEntries = [];
menuEntries.push({name: "Calculate blocks", functionName: "writeBlocks"});
ss.addMenu("Custom functions", menuEntries);
}
Save it all, refresh the spreadsheet, and there should be a new option on the menu bar. When you select that option, it will clear the old Block Nos and generate new ones based on the current inputs. Hope this helps.