I can't get text from RichTextValue - google-apps-script

I can't get a text value from RichTextValue which is from a date cell.
The target sheet looks like this:
| date | string | string |
I want to get a text value from first cell.
So I write this code:
const dataRange = sheet.getDataRange().getRichTextValues();
dataRange.forEach((row) => {
const text = row[0].getText();
Logger.log(text);
});
But the log was empty. I can't get a text value from only date cell.(I can get texts from other cells like row[1] or row[2])
How can I get a text value from a date cell?

Issue and workaround:
In the current stage, it seems that with getRichTextValues, the date object and the numbers cannot be retrieved. When those values are retrieved, no values are returned. About "getRichTextValues doesn't retrieve number values", this has already been reported to Google issue tracker. Ref I thought that this issue tracker might be the same with your situation.
I thought that this might be related to that the text format of the part of characters of the date object and number cannot be changed as "RichText".
So, in the current stage, when you want to retrieve the date object as the string value, how about using getDisplayValues instead of getRichTextValues? When your script is modified, it becomes as follows.
Modified script:
const dataRange = sheet.getDataRange().getDisplayValues();
dataRange.forEach((row) => {
const text = row[0];
Logger.log(text);
});
References:
Google issue tracker: getRichTextValues doesn't retrieve number values
getDisplayValues()

You can set formats all cells to String, get their rich values, and restore formats back. Here is the code:
function myFunction() {
const sheet = SpreadsheetApp.getActiveSheet();
const range = sheet.getDataRange();
// store orig formats of cells
const formats_orig = range.getNumberFormats();
// set all cells to String
const formats_string = formats_orig.map(row => row.map(cell => '#'));
range.setNumberFormats(formats_string);
// or you can do it this way:
// range.setNumberFormat('#');
// grab rich values
const dataRange = range.getRichTextValues();
dataRange.forEach((row) => {
const text = row[0].getText();
Logger.log(text);
});
// restore formats
range.setNumberFormats(formats_orig);
}
Perhaps it makes sense to apply String format to the first column only. I suppose you know how it can be done. Just in case:
const formats_string = formats_orig.map(x => ['#'].concat(x.slice(1)));
// or
sheet.getRange('A:A').setNumberFormat('#');

Related

Google Apps Script - Better way to do a Vlookup

I am doing a kind of VLOOKUP operation in a column with about 3K cells. I am using the following function to do it. I commented on what the code is doing in the function, but to summarize:
It creates a map from values to search for from a table with metadata
It iterates each value of a given range, and searches for coincidences in the previous map
If coincidences are found, it uses the index to capture the second column of the metadata table
Finally, sets the value captured in another cell
This is the code:
function questions_categories() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("data_processed");
// get metadata. This will work as the table to look into
// Column B contains the matching element
// Column C contains the string to return
var metadata = ss.getSheetByName("metadata").getRange('B2:C').getValues()
// Just get the different values from the column B
var dataList = metadata.map(x => x[0])
// Used to define the last cell where to apply the vlookup
var Avals = sheet.getRange("A1:A").getValues();
var Alast = Avals.filter(String).length;
// define the range to apply the "vlookup"
const questions_range = sheet.getRange("Q2:Q" + Alast);
forEachRangeCell(questions_range, (cell) => {
var searchValue = cell.getValue();
// is the value to search in the dataList we defined previously?
var index = dataList.indexOf(searchValue);
if (index === -1) {
// if not, throw an error
throw new Error('Value not found')
} else {
// if the value is there, use the index in which that appears to get the value of column C
var foundValue = metadata[index][1]
// set the value in two columns to the right
cell.offset(0, 2).setValue(`${foundValue}`);
}
})
}
forEachRangeCell() is a helper function to iterate through the range.
This works very well, but it resolves 3-4 cells per second, which is not very efficient if I need to check thousands of data. I was wondering if there is a more performant way to achieve the same result.
To improve performance, use Range.setValues() instead of Range.setValue(), like this:
function questions_categories() {
const ss = SpreadsheetApp.getActive();
const source = { values: ss.getRange('metadata!B2:C').getValues() };
const target = { range: ss.getRange('data_processed!Q2:Q') };
source.keys = source.values.map(row => row[0]);
target.keys = target.range.getValues().flat();
const result = target.keys.map(key => [source.values[source.keys.indexOf(key)]?.[1]]);
target.range.offset(0, 2).setValues(result);
}
See Apps Script best practices.

sheet.range().getRichTextValue() returns null even though it is correctly set

I'm having a problem with rich text values in spreadsheets. The following code sets the rich text value (a URL) in a cell but getting the rich text value returns null. It appears that the problem is related to the fact that the rich text value text is numeric.
function SheetEdit(e) {
let sheet = e.source.getActiveSheet();
let row = e.range.getRow();
let col = e.range.getColumn();
let year = 2022;
// The following getRichTextValue() returns correctly. Its text value is non-numeric.
let folderId = sheet.getRange(row,1).getRichTextValue().getLinkUrl().split('/')[5];
let folder = DriveApp.getFolderById(folderId);
let filelist = folder.getFilesByName(fileName);
if (filelist.hasNext()==true) {
let file = filelist.next();
let url = file.getUrl().split('?')[0]; // remove URL parameters
// The following setRichTextValue() correctly sets the value in the spreadsheet.
// Its text value (year) is numeric.
let richText = SpreadsheetApp.newRichTextValue().setText(String(year)).setLinkUrl(url).build();
sheet.getRange(row,col).setRichTextValue(richText);
// However, the following getRichTextValue() returns null
Logger.log(sheet.getRange(row,col).getRichTextValue().getText());
Logger.log(sheet.getRange(row,col).getRichTextValue().getLinkUrl());
}
}
This appears to be a known issue. One way to work around it is to change
SpreadsheetApp.newRichTextValue().setText(String(year))
to
SpreadsheetApp.newRichTextValue().setText("'"+year)
This will cause the year (number) to be interpreted/displayed by the spreadsheet as text (without the leading ') rather than as a number. When you call
sheet.getRange(row,col).getRichTextValue().getText()
you'll get 2022 as expected.

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

Is it possible to use the setNumberFormat in a custom function?

I am trying to create a custom function that will take in a number (or string) in another cell and format it in a certain way based on a second argument, which indicates what the format should be.
If I were to accomplish this task with a default Google Sheets function, I can easily achieve this by using the "text" function. Although there are only limited types of formats that I work with, using this function alone is inconvenient as I would need to rewrite the formula every time there is a line that does not conform to the same format as the number right above.
Also, there are times and situations when I would need to use a different or additional function to achieve my desired outcome and hence the effort to create a custom function for this.
The issue with the code that I came up with below (and it doesn't account for all the cases that I would like to ultimately write out) is that it will ultimately return an error:
"Exception: You do not have permission to call setNumberFormat (line 21)."
And I understand that this happens as the custom function is trying to change the format of a cell outside of its own cell. But I can't seem to find any method that will take a string value, format it, and return the formatted string.
I am linking a Sheet that shows what I'm trying to accomplish.
/**
* Formats texts or number based on the argument passed into the function with pre-existing formats
*
* #param {range} inputData The number or text that needs to be formatted
* #param {string} textType Description of the format
* #return A formated number or text
* #customFunction
*/
function customText(inputData, textType) {
//check if there are two arguments
//if(arguments.length !== 2){throw new Error("Must have 2 arguments")};
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sheet = ss.getActiveSheet()
var inputDataRange = sheet.getRange(inputData);
switch (textType)
{
case 'Shows numbers with thousand seperator and two decimal points. "-" sign for negative numbers. Blank for 0.':
var result = inputDataRange.setNumberFormat("#,#.00;-#,#.00;;#");
return result
break;
case 'Shows percentage with "%" sign. Shows up to 2 decimal points.':
var result = inputDataRange.setNumberFormat("0.00%;-#,#;0.00%;#")
return result
break;
}
}
Answer:
You're right in so much that it's not possible to do this with a custom function, but you could make a custom button which formats a highlighted range.
More Information:
Your code would need a small modification so that it can see the current highlighted range as an input rather than a range via a custom formula. From here, as the function would be run without restrictions, the .set* methods of SpreadsheetApp can be used to modify the number format of the cells in the highlighted range.
Code Modifications:
Firstly, your function will no longer need parameters to be passed to it:
function customText() {
// ...
}
and instead, we can simply take the highlighted range of cells and from this, separate out the input data and the text type:
var range = SpreadsheetApp.getActiveRange();
var dataRange = range.getValues();
var inputDataRange = dataRange.map(x => x[0]);
var textType = dataRange.map(x => x[1]);
You will also need to store the start row and column indicies of the range, as well as the sheet for which you are editing, under the assumption that your Spreadsheet has more than one sheet:
var currSheet = range.getSheet()
var startRow = range.getRow();
var startColumn = range.getColumn();
We can then loop through each element of the textType array and set the formatting of corresponding cell from inputDataRange:
textType.forEach(function(tt, index) {
switch (tt) {
case 'Shows numbers with thousand seperator and two decimal points. "-" sign for negative numbers. Blank for 0.':
currSheet.getRange(startRow + index, startColumn)setNumberFormat("#,#.00;-#,#.00;;#");
return;
case 'Shows percentage with "%" sign. Shows up to 2 decimal points.':
currSheet.getRange(startRow + index, startColumn)setNumberFormat("0.00%;-#,#;0.00%;#")
return;
}
});
Assigning the Full Function to a Button:
The full code will now look like this:
function customText() {
var range = SpreadsheetApp.getActiveRange();
var dataRange = range.getValues();
var inputDataRange = dataRange.map(x => x[0]);
var textType = dataRange.map(x => x[1]);
var currSheet = range.getSheet()
var startRow = range.getRow();
var startColumn = range.getColumn();
textType.forEach(function(tt, index) {
switch (tt) {
case 'Shows numbers with thousand seperator and two decimal points. "-" sign for negative numbers. Blank for 0.':
currSheet.getRange(startRow + index, startColumn, 1, 1).setNumberFormat("#,#.00;-#,#.00;;#");
return;
case 'Shows percentage with "%" sign. Shows up to 2 decimal points.':
currSheet.getRange(startRow + index, startColumn, 1, 1).setNumberFormat("0.00%;-#,#;0.00%;#");
return;
}
});
}
And you can create an in-sheet button which will run the script whenever you click it.
Go to the Insert > Drawing menu item and create a shape; any shape will do, this will act as your button.
Press Save and Close to add this to your sheet.
Move the newly-added drawing to where you would like. In the top-right of the drawing, you will see the vertical ellipsis menu (⋮). Click this, and then click Assign script.
In the new window, type customText and press OK.
Demo:
References:
Class SpreadsheetApp - getActiveRange() | Apps Script | Google Developers