How to get Google Sheets reorder based on points? - google-apps-script

I need help with my table.
I need to reorder my table (range: DY23:DZ33) in points by descending order.
But when I run my script I get an error: Cannot read property 'range' of undefined at onEdit(Code:2:19)
The code:
function onEdit(e) {
const range = e.range;
const sheet = range.getSheet();
if (sheet.getSheetName() == "Test1" && range.getRow() >= 23 && range.getRow() <= 33 && range.getColumn() == 131) {
sheet.getRange("DY23:EA33").sort({column: 131, ascending: true});
}
}
The tabel: https://docs.google.com/spreadsheets/d/1v9oqRNfmsVvrpZmiolOIyyuGx-OALyIYPPOXsa5znRg/edit#gid=1473685733
NB! Please forgive me for my poor english.

Issue and workaround:
I think that your error of Cannot read property 'range' of undefined at onEdit is due to that you might directly run the function onEdit. I think that this might be the reason for your current issue.
In your script, when the cells of "EA23:EA33" are edited, the script is run. But when I saw your sample Spreadsheet, the cells of "EA23:EA33" are the formulas. In this case, when the values by the formulas are changed, the function onEdit is not run. I think that this might be the reason for your 2nd issue.
And also, when you directly sort the cells of "EA23:EA33" using the sort method, the ranges of formulas in the cells are changed. By this, the values are changed. I think that this might be the reason for your 3rd issue.
When you want to run the script using the script editor, the button, and the custom menu, it is required to change the function without using the event object. So how about the following modification?
Modified script:
function sample() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Test1");
const range = sheet.getRange("EA23:EA33");
const formulas = range.getFormulas();
const mFormulas = formulas.map(([ea]) => [ea.replace(/=SUM\(([A-Z]+)([0-9]+)\+([A-Z]+)([0-9]+)\)/i, "=SUM($$$1$$$2+$$$3$$$4)")]);
if (JSON.stringify(formulas) != JSON.stringify(mFormulas)) range.setFormulas(mFormulas);
SpreadsheetApp.flush();
sheet.getRange("DY23:EA33").sort({ column: 131, ascending: true });
}
In this modified script, you can directly run this function with the script editor.
In this modified script, the formulas of cells of "EA23:EA33" are changed by the absolute reference. And the cells "DY23:EA33" are sorted. By this, the formulas can work by the sort.
References:
getFormulas()
setFormulas(formulas)

Related

How to set a named range for a data validation programmatically (in Google apps script) in a Google spreadsheet?

Use Case
Example. I have a named range Apples (address "Sheet10!B2:B"), which in use for data validation for plenty of sheet cells. The data range for Apples can be changed (in a script), e.g. to "Sheet10!D2:D".
It works from UI
I can set manually a named range as a data source of data validation.
In this case, the data validation of a cell will always refer to the named range Apples with updated the data range.
How to make it in Google Apps Script?
GAS Limits
The code, for setting data validation, should look like this, if you have a namedRange object:
mySheet.getRange('F5')
.setDataValidation(
SpreadsheetApp.newDataValidation()
.requireValueInRange(
namedRange.getRange()
)
.setAllowInvalid(false)
.build()
);
DataValidationBuilder.requireValueInRange() does not work here as it requires only class Range (it cannot get NamedRange), and no reference to a named range will be used.
Is there a workaround or so?
UPD1 - Spreadsheet.getRangeByName() does not work
Getting range by name does not help, the data validation will get actual range address.
SpreadsheetApp.getActive().getRangeByName("Apples")
UPD2 No way to make it so far in GAS
As #TheMaster posted, it's not possible at this moment.
Please set +1 for posts:
https://issuetracker.google.com/issues/143913035
https://issuetracker.google.com/issues/203557342
P.S. It looks like the only solution will work is Google Sheets API.
I thought that in your situation, I thought that when Sheets API is used, your goal might be able to be used.
Workaround 1:
This workaround uses Sheets API.
Usage:
1. Prepare a Google Spreadsheet.
Please create a new Google Spreadsheet.
From Example. I have a named range Apples (address "Sheet10!B2:B"), which in use for data validation for plenty of sheet cells. The data range for Apples can be changed (in a script), e.g. to "Sheet10!D2:D"., please insert a sheet of "Sheet10" and put sample values to the cells "B2:B" and "D2:D".
Please set the named range Sheet10!B2:B as Apple.
2. Sample script.
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And, please enable Sheets API at Advanced Google services.
function myFunction() {
const namedRangeName = "Apple"; // Please set the name of the named range.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Sheet10");
const requests = [{ updateCells: { range: { sheetId: sheet.getSheetId(), startRowIndex: 0, endRowIndex: 1, startColumnIndex: 0, endColumnIndex: 1 }, rows: [{ values: [{ dataValidation: { condition: { values: [{ userEnteredValue: "=" + namedRangeName }], type: "ONE_OF_RANGE" }, showCustomUi: true } }] }], fields: "dataValidation" } }];
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
}
In this request, the name of the named range is directly put to userEnteredValue.
3. Testing.
When this script is run to the above sample Spreadsheet, the following result is obtained.
When this demonstration is seen, first, you can see the named range of "Apple" which has the cells "B1:B1000". When a script is run, data validation is put to the cell "A1" with the named range of "Apple". In this case, the values of data validation indicate "B1:B1000". When the range named range "Apple" is changed from "B1:B1000" to "D1:D1000" and the data validation of "A1" is confirmed, it is found that the values are changed from "B1:B1000" to "D1:D1000".
Workaround 2:
This workaround uses the Google Spreadsheet service (SpreadsheetApp). In the current stage, it seems that the Google Spreadsheet service (SpreadsheetApp) cannot directly achieve your goal. This has already been mentioned in the discussions in the comment and TheMaster's answer. When you want to achieve this, how about checking whether the range of the named range is changed using OnChange as following workaround 2?
Usage:
1. Prepare a Google Spreadsheet.
Please create a new Google Spreadsheet.
From Example. I have a named range Apples (address "Sheet10!B2:B"), which in use for data validation for plenty of sheet cells. The data range for Apples can be changed (in a script), e.g. to "Sheet10!D2:D"., please insert a sheet of "Sheet10" and put sample values to the cells "B2:B" and "D2:D".
Please set the named range Sheet10!B2:B as Apple.
2. Sample script.
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And, please install OnChange trigger to the function onChange.
First, please run createDataValidation. By this, data validation is put to the cell "A1" of "Sheet10". In this case, the set range is the range retrieved from the named range "Apple". So, in this case, the range is Sheet10!B2:B1000.
As the next step, please change the range of the named range from Sheet10!B2:B1000 to Sheet10!D2:D1000. By this, onChange` function is automatically run by the installed OnChange trigger. By this, the data validation of "A2" is updated. By this, the values of data validation are changed.
const namedRangeName = "Apple"; // Please set the name of the named range.
const datavalidationCell = "Sheet10!A2"; // As a sample. data validation is put to this cell.
function onChange(e) {
if (e.changeType != "OTHER") return;
const range = e.source.getRangeByName(namedRangeName);
const a1Notation = `'${range.getSheet().getSheetName()}'!${range.getA1Notation()}`;
const prop = PropertiesService.getScriptProperties();
const previousRange = prop.getProperty("previousRange");
if (previousRange != a1Notation) {
const rule = SpreadsheetApp.newDataValidation().requireValueInRange(e.source.getRangeByName(namedRangeName)).setAllowInvalid(false).build();
e.source.getRange(datavalidationCell).setDataValidation(rule);
}
prop.setProperty("previousRange", a1Notation);
}
// First, please run this function.
function createDataValidation() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const rule = SpreadsheetApp.newDataValidation().requireValueInRange(ss.getRangeByName(namedRangeName)).setAllowInvalid(false).build();
ss.getRange(datavalidationCell).setDataValidation(rule);
const prop = PropertiesService.getScriptProperties();
const range = ss.getRangeByName(namedRangeName);
const a1Notation = `'${range.getSheet().getSheetName()}'!${range.getA1Notation()}`;
prop.setProperty("previousRange", a1Notation);
}
References:
Method: spreadsheets.batchUpdate
UpdateCellsRequest
DataValidationRule
Currently, This seems to be impossible. This is however a known issue. +1 this feature request, if you want this implemented.
https://issuetracker.google.com/issues/143913035
Workarounds from the tracker issue creator:
If a validation rule is manually created with a NamedRange via the Sheets GUI, it can then be copied programmatically using Range.getDataValidations(), and subsequently used to programmatically create new DataValidations. DataValidations created this way maintain their connection to the NamedRange, and behave like their manually created counterparts. This demonstrates that the functionality to 'use' NamedRanges for data validation rules is already possible with Apps Scripts, but not the option to 'create' them.
As a half-answer, if you want just validation and can live without the drop-down list of valid values, you can programmatically set a custom formula that references the named range. This reference to the named range will not get expanded in the AppsScript, so future changes to the Named Range's actual range will percolate to the validator. Like so:
mySheet.getRange('F5')
.setDataValidation(
SpreadsheetApp.newDataValidation()
.requireFormulaSatisfied(
'=EQ(F5, VLOOKUP(F5, ' + namedRange.getName() + ', 1))'
)
.setAllowInvalid(false)
.build()
);
(The formula just checks that the value in the cell being tested is equal to what VLOOKUP finds for that cell, in the first column -- I'm assuming the named range content is sorted.)
Use getRangeByName()
function lfunko() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet0");
var cell = sh.getRange(1, 10);//location where datavalidation is applied
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(ss.getRangeByName("MyList")).build();
cell.setDataValidation(rule);
}

Converting Google Sheets IF formula to Apps script

As a test, I have entered the following formula in cell K2 of my spreadsheet: =IF($M2=today(),"Today"). This acheives the desired effect and I would like this to be applied to all the rows below (with m3 referring to k3 , m4 to k4 etc.) , HOWEVER, this sheet is updated via Google Form so I cannot leave a formula in these cells as it will be overwritten.
Therefore I need to write and run the formula in apps script but, whilst I have enough knowledge of script language to do write basic If functions, this one is beyond my skills.
I have referred to this: How to get range and then set value in Google Apps Script and tried to adapt it to my purposes but to no avail.
Could someone please enlighten me?
Try this script:
function isMToday() {
sheet = SpreadsheetApp.getActiveSheet();
lastRow = sheet.getLastRow();
// get M2:M range
mRange = sheet.getRange(2, 13, lastRow - 1, 1);
// get display values instead to avoid timezone issues
mValues = mRange.getDisplayValues();
today = new Date();
// check every mValue, if today, return today, else false
output = mValues.map(mValue => {
mValueDate = new Date(mValue);
if (mValueDate.getDate() == today.getDate() &&
mValueDate.getMonth() == today.getMonth() &&
mValueDate.getFullYear() == today.getFullYear())
return ["Today"];
else
return [false];
});
// write output to K2:K
mRange.offset(0, -2).setValues(output);
}
After execution:
You do not need AppScript to do this even though it is on a Form Responses tab.
Instead of this formula in cell K2 (as you described):
=IF($M2=today(),"Today")
Use this formula in cell K1, and delete all the other formulas in column K:
=ARRAYFORMULA(IF(ROW(M:M)=1,"Today?",IF(M:M=TODAY(),"Today")))

How to get google sheets to reorder based on ranking

I have created a Google Sheet that has a "days since" rank based on differences from todays date to date of the last "incident".
These days update automatically, but what I want is to have a ranking system where the Google Sheet reorders if the date of someone's last incident changes.
Is this possible? I have attached the link below, but it is a very simple formula as of now.
Row D: =today()-C3
https://docs.google.com/spreadsheets/d/12SUfrB7XuqGlfxjvaePHJTGaZz_FhjiWhLxjPMAM2jI/edit?usp=sharing
If you don't want to use Google Apps Script you may duplicate this table (in sheet2 for example) and have it's content duplicated there within sort() function.
Lets say you put
=sort(Sheet1!A3:D8,4,0) in A3 cell of new Sheet2
I know it's not exactly the goal...
I believe your goal as follows.
You want to automatically sort the data with the column "C" when users edited the cells "C3:C8".
The table of the data is the cells "A3:D8".
From your tag of google-apps-script, I thought that your direction includes to use Google Apps Script.
In this case, I would like to propose to use OnEdit simple trigger for achieving your goal. The sample script is as follows
Sample script:
Please copy and paste the following script to the container-bound script of the Spreadsheet and save it.
function onEdit(e) {
const range = e.range;
const sheet = range.getSheet();
if (sheet.getSheetName() == "Sheet1" && range.getRow() >= 3 && range.getRow() <= 8 && range.getColumn() == 3) {
sheet.getRange("A3:D8").sort({column: 3, ascending: true});
}
}
When you edit the cells "C3:C8", the script is automatically run by the OnEdit simple trigger, and the data is sorted with the column "C".
Result:
When your shared Spreadsheet is used with above sample script, the following result can be obtained.
Note:
When you want to sort with the column "D", please modify above script as follows.
From
sheet.getRange("A3:D8").sort({column: 3, ascending: true});
To
sheet.getRange("A3:D8").sort({column: 4, ascending: false});
References:
Simple Triggers
sort(sortSpecObj)

Run google script when specific cell has specific number

I have a sheet called 'Admin' where I created a drop down with 4 options. Upon selecting the drop down, the 'trigger' column (3) will display a number.
In scripts, I have 4 scripts written and tested, but I want these to run based on the value displayed in the 'Trigger' column above:
Could anyone help me write this script, as I have tried numerous scripts but cannot seem to get anywhere close?
The sheet id is:
https://docs.google.com/spreadsheets/d/1doHHOSc3GkMX5jVayU63-PDXQ1Q4s4CepbMW6uXS3DY/edit?usp=sharing
My thanks in advance,
Brendon
I believe your goal as follows.
You want to run the function of the function name corresponding to the cell "C2" of the sheet "Admin".
For example, the value of cell "C2" is 1, you want to run the function of One().
In your case, how about using the OnEdit event trigger? But from your script, I think that it is required to use the OnEdit event trigger as the installable trigger.
Sample script:
Please copy and paste the following script to the script editor and install the OnEdit event trigger to the function of installedOnEdit.
function installedOnEdit(e) {
var range = e.range;
var sheet = range.getSheet();
if (sheet.getSheetName() == "Admin" && range.getA1Notation() == "A2") {
var functions = {1: One, 2: Two, 3: Three, 4: Four};
functions[range.offset(0, 2).getValue()]();
}
}
By above settings, when you change the dropdown list of the cell "A2" on the sheet "Admin", the function corresponding to the value of cell "C2" is run.
Note:
In this modification, it supposes that your functions of One, Two, Three and Four work fine. Please be careful this.
When you modified the function names you want to run, please also modify var functions = {1: One, 2: Two, 3: Three, 4: Four};.
Reference:
Installable Triggers
Added:
When you want to directly run the function of installedOnEdit with the script editor, at first, please check the following points.
In this case, it is required to fix the cell for using the function. So in your case, it supposes that the cell of "C2" on the sheet of "Admin" are always used. When you use other cells, please modify the following script for your situation. Please be careful this.
Modified script:
function installedOnEdit(e) {
var functions = {1: One, 2: Two, 3: Three, 4: Four};
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Admin");
var value = sheet.getRange("C2").getValue();
functions[value]();
}

Custom function won't refresh as inputs are changed

I have a custom function that finds the value of another cell and displays it. When the source cell is changed, the function does not reflect.
https://docs.google.com/spreadsheets/d/1wfFe__g0VdXGAAaPthuhmWQo3A2nQtSVUhfGBt6aIQ0/edit?usp=sharing
Refreshing google sheets
function findRate() {
var accountName = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(1,1).getValue(); //determine the account name to use in the horizontal search
var rateTab = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Rates'); //hold the name of the rate tab for further dissection
var rateNumColumns =rateTab.getLastColumn(); //count the number of columns on the rate tab so we can later create an array
var rateNumRows = rateTab.getLastRow(); //count the number of rows on the rate tab so we can create an array
var rateSheet = rateTab.getRange(1,1,rateNumRows,rateNumColumns).getValues(); //create an array based on the number of rows & columns on the rate tab
var currentRow = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveCell().getRow(); //gets the current row so we can get the name of the rate to search
var rateToSearch = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(currentRow,1).getValue(); //gets the name of the rate to search on the rates tab
for(rr=0;rr<rateSheet.length;++rr){
if (rateSheet[rr][0]==rateToSearch){break} ;// if we find the name of the
}
for(cc=0;cc<rateNumColumns;++cc){
if (rateSheet[0][cc]==accountName){break};
}
var rate = rateSheet[rr][cc] ; //the value of the rate as specified by rate name and account name
return rate;
}
If I change a rate in the rate tab, I need the custom function to recognize the new rate and update its value
You want to recalculate the custom function of =findRate(), when the cells of the sheet name of Rates are edited.
If my understanding is correct, how about adding the following sample script? Please think of this as just one of several answers.
Solution:
In order to recalculate the custom function, in this answer, the formula of =findRate() is overwritten by the script running with the OnEdit event trigger (in this case, it's the simple trigger.). By this, the recalculate is executed. But, when the formula is directly replaced by the same formula, the recalculate is not executed. So I used the following flow.
Retrieve all ranges of cells which have the formula of =findRate() from the sheet of "Projected Revenue".
Clear the formulas of the ranges.
Put the formulas to the ranges.
By this flow, when the cell of the sheet of "Rates" is edited, the custom function of =findRate() is recalculated by automatically running onEdit().
Sample script:
Please copy and paste the following script to the script editor. Then, please edit the cells of sheet name of Rates. By this, onEdit() is automatically run by the OnEdit event trigger.
function onEdit(e) {
var range = e.range;
if (range.getSheet().getSheetName() == "Rates" && range.rowStart > 1 && range.columnStart > 1) {
var sheetName = "Projected Revenue"; // If you want to change the sheet name, please modify this.
var formula = "=findRate()";// If you want to change the function name, please modify this.
var sheet = e.source.getSheetByName(sheetName);
var ranges = sheet.createTextFinder(formula).matchFormulaText(true).findAll().map(function(e) {return e.getA1Notation()});
sheet.getRangeList(ranges).clearContent();
SpreadsheetApp.flush();
sheet.getRangeList(ranges).setFormula(formula);
}
}
Note:
onEdit(e) is run by the OnEdit event trigger. So when you directly run onEdit(e), an error occurs. Please be careful this.
In this sample script, as a sample, even when the row 1 and column "A" of the sheet of "Rates" are edited, the custom function is not recalculated. If you want to modify this and give the limitation of range you want to edit, please modify the above script.
References:
Simple Triggers
Class TextFinder
Class RangeList
flush()
If I misunderstood your question and this was not the result you want, I apologize.
Added:
The proposal from TheMaster's comment was reflected to the script. When sheet.createTextFinder(formula).matchFormulaText(true).replaceAllWith(formula) can be used, also I think that the process cost will be much reduced. But in my environment, it seemed that the formulas are required to be cleared once to refresh the custom function, even if flush() is used. So I have proposed above flow.
But, now I could notice a workaround using replaceAllWith() of TextFinder. So I would like to add it. The flow of this workaround is as follows.
Replace all values of =findRate() to a value in the sheet of Projected Revenue using replaceAllWith()..
In this case, as a test case, the formulas are replaced to sample.
Replace sample to =findRate() using replaceAllWith().
By this flow, I could confirm that =findRate() is recalculated. And also, it seems that flush() is not required for this situation.
Sample script:
Please copy and paste the following script to the script editor. Then, please edit the cells of sheet name of Rates. By this, onEdit() is automatically run by the OnEdit event trigger.
function onEdit(e) {
var range = e.range;
if (range.getSheet().getSheetName() == "Rates" && range.rowStart > 1 && range.columnStart > 1) {
var sheetName = "Projected Revenue";
var formula = "=findRate()";
var tempValue = "sample";
var sheet = e.source.getSheetByName(sheetName);
sheet.createTextFinder(formula).matchFormulaText(true).replaceAllWith(tempValue);
sheet.createTextFinder(tempValue).matchFormulaText(true).replaceAllWith(formula);
}
}