Add link to entire column with appsscript - google-apps-script

I'm retrieving information from an API and have to link the ID to the website.
I have the following code that is working but right now I'm retrieving 150 results and are expecting to have over 650. With 150 it takes two minutes to iterate through the API call and this for.
for (var i = 2; i <= count; i++) {
var forRange = sheet.getRange("A"+i)
var forValue = forRange.getValue();
const richText = SpreadsheetApp.newRichTextValue()
.setText(forValue)
.setLinkUrl('https://link.com/view.php?id=' + forValue)
.build();
forRange.setRichTextValue(richText);
}
My question is: Is there a more optimized way to do this iteration?

In your script, getValue() and setRichTextValue() are used in a loop. In this case, the process cost will become high. I think that this is the reason for your issue. In this case, how about the following modification?
Modified script:
const sheet = SpreadsheetApp.getActiveSheet(); // Please use your Sheet object here.
const range = sheet.getRange("A2:A" + sheet.getLastRow());
const values = range.getDisplayValues();
const obj = range.getRichTextValues().map(([a], i) => [a.copy().setText(values[i][0]).setLinkUrl('https://link.com/view.php?id=' + values[i][0]).build()]);
range.setRichTextValues(obj);
When this script is run, the object of RichTextValue is created for each row as an array. And, the created RichTextValue is used with setRichTextValues. By this, I thought that the process cost can be reduced.
Note:
In the current stage, getRichTextValues() cannot retrieve the number value from the cells by the current specification. Ref When I saw your sample Spreadsheet, it was found that the values of column "A" are the number values. This is the current issue. So, I modified my proposed script.
References:
setRichTextValues(values)
map()

Related

App Script in Google Sheets - problem with data auto complete

I have two sheets. In one, the data of the manufactured devices is already entered. In the second, new warranty claims are added daily. To shorten the working time, I automatically copy the production data from the sheet Production to sheet Complaints. Currently I am solving this with VLOOKUP or INDEX function. But it works very slow when number of rows >20k.
Is it possible to write a script (using AppScripts) where after entering the ID number in the complaint sheet and selecting this cell, script will independently fill in the appropriate columns with data from 'Production'? (the order of the columns is different in both sheet)
Link to my sheet with example
I've tried all option with built-in function, I'm expecting to know and understand a possible solution using AppScript
In your situation, how about the following sample script?
Sample script:
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And please confirm the sheet names again.
function myFunction() {
// Retrieve sheets.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const productionSheet = ss.getSheetByName("Production");
const complaintsSheet = ss.getSheetByName("Complaints");
// Retrieve values from sheets.
const [h1, ...v1] = productionSheet.getDataRange().getValues();
const range = complaintsSheet.getRange("A2:I" + complaintsSheet.getLastRow());
const [h2, ...v2] = range.getValues();
// Create an array for putting to Complaints sheet by converting columns.
const hconv = h2.map(h => h1.indexOf(h));
const obj = v1.reduce((o, r) => (o[r[0]] = hconv.map(i => i > -1 ? r[i] : null), o), {});
const len = Object.values(obj)[0].length;
const res = v2.map(([a]) => obj[a] || Array(len).fill(null));
// Put the created array to Complaints sheet.
range.offset(1, 0, res.length, res[0].length).setValues(res);
}
When this script is run, the values are retrieved from both sheets. And, an array is created for putting to "Complaints" sheet by checking the ID of column "A" and converting the columns. And, the array is put to "Complaints" sheet.
Note:
In this sample, I tested it using your sample Spreadsheet. If you change the spreadsheet, this script might not be able to be used. Please be careful about this.
I thought that in this case, the custom function might be able to be used. But, from your question, if the data is large, the custom function cannot be used. So, I proposed the above script.
In this sample, in order to convert the columns, the header titles are used. So, if you change the header titles, please be careful about this.
As another approach, if you want to put the value to the row when you put a value to the column "A" of "Complaints", how about the following script? In this script, the script is automatically run by a simple trigger of OnEdit. So, in this case, when you put a value to the column "A" of "Complaints" sheet, this script is automatically run, and the value is put into the edited row. Please select one of 2 scripts for your actual situation. As an important point, when you use this, please don't directly run this function because of no event object. Please be careful about this.
function onEdit(e) {
const { range, source } = e;
const complaintsSheet = range.getSheet();
if (complaintsSheet.getSheetName() != "Complaints" || range.columnStart != 1 || range.rowStart <= 2) return;
const value = range.getValue();
const productionSheet = source.getSheetByName("Production");
const [h1, ...v1] = productionSheet.getDataRange().getValues();
const [h2] = complaintsSheet.getRange("A2:I2").getValues();
const hconv = h2.map(h => h1.indexOf(h));
const obj = v1.reduce((o, r) => (o[r[0]] = hconv.map(i => i > -1 ? r[i] : null), o), {});
const len = Object.values(obj)[0].length;
const res = [obj[value] || Array(len).fill(null)];
range.offset(0, 0, res.length, res[0].length).setValues(res);
}
References:
reduce()
map()

How to get range that has been moved?

Let's say I am getting the range A2:B2:
sheet.getRange("A2:B2").getValues();
But I added a row under A1:B1, so now my values are in A3:B3
Is it possible for Apps Script to dynamically catch that my values are now on another range ?
If not, any alternative ideas on how I can dynamically get the range of moving rows ?
I believe your goal is as follows.
You want to know whether the values of a range of "A2:B2" is moved.
In this case, how about using the named range, OnChange trigger, and PropertiesService? I thought that when those are used, your goal might be able to be achieved. When the sample script is prepared, it becomes as follows.
Usage:
1. Create a named range.
As a sample, please create a named range to the cells "A2:B2" as "sampleNamedRange1". Ref
2. Prepare sample script.
Please copy and paste the following script to the script editor of Spreadsheet. And, please install OnChange trigger to the function installedOnChange.
function installedOnChange(e) {
var originalRange = "A2:B2";
var nameOfNamedRange = "sampleNamedRange1";
if (!["INSERT_ROW", "REMOVE_ROW"].includes(e.changeType)) return;
var p = PropertiesService.getScriptProperties();
var pv = p.getProperty("range");
if (!pv) pv = originalRange;
var range = e.source.getRangeByName(nameOfNamedRange);
var a1Notation = range.getA1Notation();
if (a1Notation != pv) {
p.setProperty("range", a1Notation);
Browser.msgBox(`Range was changed from ${pv} to "${a1Notation}". Original range is "${originalRange}".`);
}
// var values = range.getValues(); // The values are not changed because of the named range.
}
3. Testing.
In this case, please do I added a row under A1:B1. By this, the script is automatically run by the OnChange trigger. And, you can see the dialog. You can see the demonstration as shown in the following image.
Note:
This is a simple sample script. So, please modify this for your actual situation.
References:
Installable Triggers
getRangeByName(name)
Properties Service
Added:
From the discussions, I understood that you wanted to retrieve the named range of the specific sheet using the name of the named range. In this case, the sample script is as follows.
When the sheet name is used
const range = SpreadsheetApp.getActive().getSheetByName("sheetName").getRange("nameOfNamedRange");
or
const range = SpreadsheetApp.getActive().getSheetByName("sheetName").getNamedRanges().find(n => n.getName() == "nameOfNamedRange").getRange();
When the active sheet is used
const range = SpreadsheetApp.getActiveSheet().getRange("nameOfNamedRange");
or
const range = SpreadsheetApp.getActiveSheet().getNamedRanges().find(n => n.getName() == "nameOfNamedRange").getRange();
This is possible through DeveloperMetadata. Metadata can be set to ranges or sheets and whenever such data(ranges or sheets) are moved, the associated metadata moves along with it as well. Unfortunately, this metadata cannot be set to arbitrary ranges, but only to single column or single row. For eg, with A2:B2, We have to set the metadata to the entirety of column A, column B and Row 2. However, once set, apps script is no more needed. Google sheets automatically keeps track of the movements of such data.
Sample script:
const setDevMetadata_ = (sheetName = 'Sheet1', rng = '2:2', key = 'a2b2') => {
SpreadsheetApp.getActive()
.getSheetByName(sheetName)
.getRange(rng)
.addDeveloperMetadata(key);
};
/**
* #description Set metadata to a specific range
* Unfortunately, this metadata cannot be set to arbitrary ranges, but only to single column or single row.
* For eg, with `A2:B2`, We have to set the metadata to the entirety of column A, column B and Row 2.
* #see https://stackoverflow.com/a/73376887
*/
const setDevMetadataToA2B2 = () => {
['2:2', 'A:A', 'B:B'].forEach((rng) => setDevMetadata_(undefined, rng));
};
/**
* #description Get a range with specific developer metadata key
*/
const getRangeWithKey = (sheetName = 'Sheet1', key = 'a2b2') => {
const sheet = SpreadsheetApp.getActive().getSheetByName(sheetName),
devFinder = sheet.createDeveloperMetadataFinder(),
[rows, columns] = ['Row', 'Column'].map((rc) =>
devFinder
.withKey(key)
.withLocationType(
SpreadsheetApp.DeveloperMetadataLocationType[rc.toUpperCase()]
)
.find()
.map((devmetadata) =>
devmetadata.getLocation()[`get${rc}`]()[`get${rc}`]()
)
);
console.log({ rows, columns });
const rng = sheet.getRange(
rows[0],
columns[0],
rows[1] ? rows[1] - rows[0] + 1 : 1,
columns[1] ? columns[1] - columns[0] + 1 : 1
);
console.log(rng.getA1Notation());
return rng;
};

Google Apps Script to order columns by name when pulling from Firestore

I am pulling data from a collection of documents in Firestore and displaying this data in a Google Sheet.
Each time I run the function to get this data, the columns appear in a random order each time.
I would like the columns to appear in a specific order, or in alphabetical order.
This function gets my data and appends it to the spreadsheet:
function importData() {
const firestore = getFirestore();
const allDocuments = firestore.getDocuments('Data').map(function(document) {
return document.fields;
});
const first = allDocuments[0];
const columns = Object.keys(first);
const sheet = SpreadsheetApp.getActiveSpreadsheet();
const ss = sheet.getActiveSheet();
ss.clear();
sheet.appendRow(columns);
allDocuments.forEach(function(document) {
const row = columns.map(function(column) {
return document[column];
});
sheet.appendRow(row);
});
trimData();
formatData();
}
The trimData() function removes any useless text and white spaces such as the the field type, e.g. stringValue.
The formatData() function sets an alternating color scheme and sets the cell values to appear on the left border of each cell.
I believe your goal as follows.
You always want to put the values using the same order of the columns.
Modification points:
About your following issue,
Each time I run the function to get this data, the columns appear in a random order each time.
I thought that the reason of your issue is due to the line of const columns = Object.keys(first);. In this case, the order of keys might be changed every run. So I would like to propose the following modification.
And, in your script, appendRow is used in a loop. In this case, the process cost will become high. Ref In this case, I would like to propose to use setValues.
When above points are reflected to your script, it becomes as follows.
Modified script:
Please modify your script as follows.
From:
const first = allDocuments[0];
const columns = Object.keys(first);
const sheet = SpreadsheetApp.getActiveSpreadsheet();
const ss = sheet.getActiveSheet();
ss.clear();
sheet.appendRow(columns);
allDocuments.forEach(function(document) {
const row = columns.map(function(column) {
return document[column];
});
sheet.appendRow(row);
});
To:
const first = allDocuments[0];
const columns = Object.keys(first).sort();
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
sheet.clear();
const values = allDocuments.reduce(function(ar, document) {
const row = columns.map(function(column) {
return document[column];
});
ar.push(row);
return ar;
}, [columns.slice()]);
sheet.getRange(1, 1, values.length, values[0].length).setValues(values);
By this modification, the order of keys is always sorted, and you can use always the same order.
But, if you add other fields, the order of keys is also changed. So please be careful this.
References:
Benchmark: Reading and Writing Spreadsheet using Google Apps Script
setValues(values)
If allDocuments is an array you can sort it:
allDocuments.sort();
There are other options available for sorting arrays as well, I use this site all the time:
https://www.w3schools.com/js/js_array_sort.asp

Looking for better way to pass range for my function sumBlack()

I would love to be able use my script in Google sheet with just =sumBlack(C4:14)
currently my script ** see below ** works in Google sheet with =sumBlack(3,4,14)
3 for column, 4 and 14 are the row start and end
here's my code... pleased that it works though
it sums only cells that have the fontColor of black
function sumBlack(column, rowst, rowend) {
result = 0;
for(row = rowst;row <= rowend;row++){
var txtColor = SpreadsheetApp.getActive().getDataRange().getCell(row, column).getFontColor();
if(txtColor == "#000000"){
result = result + SpreadsheetApp.getActive().getDataRange().getCell(row, column).getValue();
}
}
return result;
}
I believe your goal as follows.
You want to sum the values of cells, when the font color is #000000 as hex.
You want to achieve this using the custom function.
Modification points:
In this case, in order to give the a1Notation to the custom function, how about using =sumBlack("C4:14") instead of =sumBlack(C4:14)? Because when =sumBlack(C4:14) is used, the values of cells "C4:14" is given as 2 dimensional array. By this, the range cannot be known.
In this modification, getFontColors() and getValues() are used instead of getFontColor()andgetValue(), respectively. By this, I think that the process cost will be able to be reduced.
When you can permit this suggestion, how about the following modified script?
Modified script:
When you use this script, please put =sumBlack("C4:14") to a cell. In this case, please don't forget to enclose the a1Notation with ".
function sumBlack(a1Notation) {
const range = SpreadsheetApp.getActiveSheet().getRange(a1Notation);
const fontColors = range.getFontColors();
const values = range.getValues();
const result = fontColors.reduce((n, r, i) => {
r.forEach((c, j) => {
if (c == "#000000") n += Number(values[i][j]);
});
return n;
}, 0);
return result;
}
If you want to give the sheet name like =sumBlack("Sheet1!C4:14"), please modify above script as follows.
From
const range = SpreadsheetApp.getActiveSheet().getRange(a1Notation);
To
const range = SpreadsheetApp.getActiveSpreadsheet().getRange(a1Notation);
In above modified script, when =sumBlack("C4:14") is put and the cell value of "C4:14" is changed, no recalculation occurs. If you want to recalculate for this situation, please add the following script. The following script is automatically run when the cells in the active sheet are edited, and the formula of =sumBlack() is recalculated.
function onEdit(e) {
const sheet = e.source.getActiveSheet();
sheet.createTextFinder("=sumBlack").matchFormulaText(true).replaceAllWith("###sumBlack");
sheet.createTextFinder("###sumBlack").matchFormulaText(true).replaceAllWith("=sumBlack");
}
References:
getFontColors()
getValues()
reduce()
Class TextFinder

Google App Script / Spreadsheet - How to get filter criteria?

Is it possible to get the filter criteria of my data set. For example, if one of my column is "Department" and I filtered the data to display only "IT". How do we get the filtered criteria, "IT". I need to get that filtered criteria into my GAS to do some other manipulation.
Thanks.
Try this example for a simple filter. It will filter information (change XXX to something meaningful) from the first column:
function onOpen(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var filterMenuEntries = [{name: "filter Column1", functionName: "filter"}];
ss.addMenu("choose filter", filterMenuEntries);
}
function filter(){
var sheet = SpreadsheetApp.getActiveSheet();
var rows = sheet.getDataRange();
var numRows = rows.getNumRows();
var values = rows.getValues();
for (var i=1; i <=numRows -1; i++){
var row =values[i];
// Column value
var myValue = row[0];
// filter value
if (myValue == "XXX"){
sheet.hideRows(i+1);
}
}
}
Google Spreadsheet already has a FILTER formula (I always use this page to remind me how to do it). So for example if your data looked like this
A
1 Department
2 IT Department (Edinburgh)
3 Department of IT
4 Other Department
to get a filtered list you could use the formula
=FILTER(A:A;FIND("IT",A:A)>0)
(Working example here)
If you want to do something entirely in Apps Script Romain Vialard has written a Managed Library with a filter function. Here are instructions for installing and using the 2D Array2 library
Filters are now available using with the recent launch of the Advanced Sheets Service:
Here is a link to the article
Here is a little snippet of how to do :
function setSheetBasicFilter(ssId, BasicFilterSettings) {
//requests is an array of batchrequests, here we only use setBasicFilter
var requests = [
{
"setBasicFilter": {
"filter": BasicFilterSettings
}
}
];
Sheets.Spreadsheets.batchUpdate({'requests': requests}, ssId);
}
In search for an answer to this question, I came up with the next workaround:
apply filters in the sheet;
color the filtered (and therefore
visible) cells (in the example code red in column F);
run script:
reading background colors (of column F) in array colors
iterating this array
building up the new array of row numbers that are visible.
This last array can be used to further manipulations of the sheet data.
To vivualize the effect of the script, I made the script setting the background of used cells to green.
It's one extra small effort for the end user to make, but IMHO it's the only way to make it possible to only use the filtered data.
function getFilterdData(){
var s = SpreadsheetApp.getActive();
var sheet= s.getSheetByName('Opdrachten en aanvragen');//any sheet
var rows = new Array();
var colors = sheet.getRange(1, 6, sheet.getLastRow(), 1).getBackgrounds();
for(var i = 0; i < colors.length; i++){
if(colors[i] == "#ff0000"){
var rowsIndex = rows.length;
rows[rowsIndex] = i+1;
sheet.getRange(i+1, 6).setBackground("#d9ead3")
}
}
}
Based on mhawksey answer, I wrote the following function:
//
function celdasOcultas(ssId, rangeA1) {
// limit what's returned from the API
var fields = "sheets(data(rowMetadata(hiddenByFilter)),properties/sheetId)";
var sheets = Sheets.Spreadsheets.get(ssId, {ranges: rangeA1, fields: fields}).sheets;
if (sheets) {
return sheets[0].data[0].rowMetadata;
}
}
then called it this way:
// i.e. : ss = spreadsheet, ranges = "Hoja 2!A2:A16"
Logger.log(range.getA1Notation());
var ocultas = celdasOcultas(ss.getId(), sheetName + "!" + range.getA1Notation());
// Logger.log(ocultas);
var values = range.getValues();
for (var r = 0; r < values.length; r++) {
if (!ocultas[r].hiddenByFilter) {
values[r].forEach( function col(c){
Logger.log(c);
});
}
//range.setBackground("lightcoral");
}
Than prevent the log of the hidden rows. You can see it in action at:
Prueba script, note project has to get Google Sheets API enabled on Google API Console.
Hope it helps. Thank you!
This as raised in Google's Issue Tracker as Issue #36753410.
As of 2018-04-12, they have posted a solution:
Yesterday we released some new functionality that makes it possible to
manipulate filters with Apps Script:
https://developers.google.com/apps-script/reference/spreadsheet/range#createFilter()
https://developers.google.com/apps-script/reference/spreadsheet/sheet#getfilter
https://developers.google.com/apps-script/reference/spreadsheet/filter
Unfortunately Apps Script still doesn't have methods for determining
if a given row or column is hidden by the filter. See comment #157 for
a workaround utilizing the Sheets Advanced Service.
The Filter class lets you get the FilterCriteria for each column in turn.