Is it possible to use the setNumberFormat in a custom function? - google-apps-script

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

Related

How do I write data in every last column?

I need to add a new column everyday and the data insertion will be done individually for the specific row comparing with the input... please tell me if it is correct or not.
will this piece of code work for adding new columns ever day and initialize all the entries by 0?
function trig(){
var builder = ScriptApp.newTrigger("addcol").timeBased().everyDays(1);
var trigger = builder.create();
}
function addcol(){
var cname = builder.atDate( day, month, year)
var column = eventRange.getLastColumn();
sheet.insertColumnAfter(column).setName(cname);
sheet.getRange("E1").setValue(new Date()).setNumberFormat('d/M/yyyy');
var col = [];
for(var n=0 ; n<s.getMaxRows();n++){
col.getLastColumn().push(['0']);
}
ss.getRange('N:N').setValues(col);
}
// now for the insertion part
here the sr will be compared to SRN from the sheet (E) and if it matches it will replace 0 with 1 in the last column added everyday. plese tell me will this work?
function doPost(e){
var action = e.parameter.action;
if(action == 'scanner'){
return scanner(e);
}
}
function scanner(e){
var srn = e.parameter.sr;
var C = sheet.getLastColumn();
var R = sheet.getLastRow();
for(i=1; i<=R; i++)
{
if (srn == sheet.getDataRange([i][2]))
{
sheet.getDataRange([i],[C]).push[(1)];
sheet.append([i],[C]);
return ContentService.createTextOutput("Success").setMimeType(ContentService.MimeType.TEXT);
break;
}
}
}
Time-based trigger:
There are no event objects associated with time-based triggers, so variables like eventRange cannot work. It seems like you want to use variables in addcol that are defined in trig (e.g. builder). That is not possible. Also, if you want your function to run once a day, there is no need for lines like this: builder.atDate(day, month, year)). The trigger will be created by running this function once:
function createTrigger(){
var builder = ScriptApp.newTrigger("addcol").timeBased().everyDays(1).create();
}
Adding column with 0's:
There are many problems with the function addcol:
Several uninitialized variables are being used (s, builder, eventRange).
Unexisting methods are being used: e.g.: setNumberFormat is a method of the Range class, not of the Date object. You should use Utilities.formatDate(date, timeZone, format) to format dates. Also, you are using setName when inserting a new column, but that changes the sheet name. Is that what you want to do? And also, cname is assigned a trigger builder as value, which I seriously doubt is your purpose. The same way, an array col does not have a method getLastColumn().
You could use this addcol function instead (change your sheet name, currently set to Your sheet name, and the timeZone in formatDate, currently set to GMT:
function addcol() {
var sheet = SpreadsheetApp.getActive().getSheetByName("Your sheet name"); // Change accordingly
var lastCol = sheet.getLastColumn();
var lastRow = sheet.getLastRow();
if (sheet.getMaxColumns() === lastCol) sheet.insertColumnAfter(lastCol);
var newCol = sheet.getRange(1, lastCol + 1, lastRow, 1);
var values = [];
values.push([Utilities.formatDate(new Date(), "GMT", "d/M/yyyy")]); // Change accordingly
for (var i = 1; i < sheet.getLastRow(); i++) {
values.push([0]);
}
newCol.setValues(values);
}
Replacing 0's with 1's:
Assuming that you are getting the function scanner to run correctly and that the parameter e.parameter.sr is getting populated correctly, you can do the following:
function scanner(e){
var srn = e.parameter.sr;
var C = sheet.getLastColumn();
var R = sheet.getLastRow();
for (i=1; i<=R; i++) {
if (srn == sheet.getRange(i, 2).getValue()) {
sheet.getRange(i, C).setValue(1);
}
}
}
Here you were also using unexisting methods or providing incorrect parameters:
The method getDataRange doesn't allow any argument, you should be using getRange(row, column), and provide the row and column indexes separated by commas, not as if trying to access a 2D array.
break terminates the current loop, so only use it if you only want to update 1 cell. The same goes for return which finishes current function execution.
Reference:
Spreadsheet Service
Installable Triggers
Short
No
Long
There are several problems with the script:
getDataRange() expects no arguments passed (docs only say it is the same as using getRange(yourSheet.getLastRow(), yourSheet.getLastColumn()), not that you should do it). Certainly it does not expect instances of Array (bracket [] notation wraps C and i, which are of type Number into one). Moreover, it returns a Range, which at the time of writing does not have push() method.
getLastColumn() returns an instance of Number, and thus does not have a push() method as well. You are on the right track, though, since col is an Array, and you need to push() into it.
If you want the script to add a zero-filled column, don't get constant ranges: in current state, getRange('N:N') guarantees that each time you will re-initialize column N. Btw, same goes for getRange("E1").
You still haven't addressed issues listed in comments to your previous question.
Also, in your scanner function there is a syntax error: push[(1)] should be push([1]).
Also, the sheet variable is either undeclared or is declared globally, which is bad.
Notes
If you don't expect number of students to change dynamically, you can switch from getMaxRows() to getLastRow() to only zero-fill cells that are in range of cureent student info grid.
This question is a direct continuation of a currently closed one (please, always disclose that for reference at least).
How about skipping init to zero step at all? If cell is empty, getValue() / getValues() will return its value as an empty string, which is a falsy value, just as 0 is. If you want to count attendance at the end of period, a simple conditional will suffice to sum up.
The default MIME type for TextOutput instance obtained by createTextOutput() is plain text, so setting it to ContentService.MimeType.TEXT is an overkill in your case.
Reference
getDataRange() docs
getLastColumn() docs
getValue() docs
getValues() docs
Range docs
createTextOutput() docs
Falsy values explanation on MDN

How to Sort Sheets Cells by values applied in GAS?

Column A of my Sheet has status values [Yes,Pending,No,Withdrawn] in the form of a dropdown. I would like to sort by these values but the order they will be sorted in is not alphabetical.
I currently use a helper column with IFS formulas in the sheet that applies a numeric value to each status and sorts them in that order. My assumption is that I can use a script to accomplish this without needing a helper column in my sheet.
From other somewhat similar questions I've gathered that this may use a compare function but my knowledge of GAS is fairly introductory.
I think using the helper column is definitely the simplest option. You can "hide" the column so it isn't in the way. (Right click -> Hide column).
But you can definitely accomplish this with a script, using a compare function! Here is an example implementation. It's a little overly verbose, in hopes of being explanatory and adaptable to your exact use case.
Code.gs
// get active spreadsheet
var ss = SpreadsheetApp.getActive();
// define mapping of status to custom values
var mapping = {
Yes: 1,
No: 2,
Pending: 3,
Withdrawn: 4
};
// define range of values to sort & which one is "status"
var sortRange = "A2:B20";
var statusCol = 0;
/**
* Sort defined range by status, using defined mapping
* See: https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet
*/
function sortData() {
// select sheet
var sheet = ss.getSheets()[0];
// select range
var range = sheet.getRange(sortRange);
// get values (array of arrays)
var data = range.getValues();
Logger.log("\ndata pre-sort: %s\n\n", data);
// sort using custom compare function
data.sort(sortFcn_);
Logger.log("\ndata post-sort: %s\n\n", data);
// write values back to spreadsheet
range.setValues(data);
}
/**
* Custom compare function used by sortRange
* See: https://www.w3schools.com/jsref/jsref_sort.asp
*/
function sortFcn_(rowA, rowB) {
// get "status" from row (array lookup by integer)
var aStatus = rowA[statusCol];
var bStatus = rowB[statusCol];
// convert status msg to value (object lookup by key)
var aValue = mapping[aStatus];
var bValue = mapping[bStatus];
// sort in ascending order
return aValue - bValue;
}
Now you just need to figure out how to call "sortData" at the right times. For a few options, see:
Container-bound Scripts
Simple Triggers
Custom Menus

Google Apps Script getRanges returns a list with a single Range

I am trying to get the range of selected cells in Google Apps Scripts and chain them into an HtmlOutput.
One of the columns in my spreadsheet has HTML input. For example: <p>Hello <em>World</em></p>. The idea is to simply give a nice preview of the HTML.
My use case is that the user selects a range of cells (multiple rows, same column) by dragging their cursor across, and then previews content for the selected rows.
I am trying to do this with this code:
function previewModal() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var ranges = sheet.getActiveRangeList().getRanges();
var html = HtmlService.createHtmlOutput('');
for (var i = 0; i < ranges.length; i++) {
var contentCell = ranges[i];
var htmlContent = contentCell.getValue();
html.append('<div>').append(htmlContent).append('</div>').append('<hr>');
}
html.setWidth(1000).setHeight(1000);
SpreadsheetApp.getUi().showModalDialog(html, 'Preview');
}
However, when I run this, I see that ranges.length is equal to 1, regardless of how many cells I have selected. And the cell that ranges contains is the "active" cell -> this is the one which you first clicked, before dragging across to cover more cells. As a result, my HTML output just contains the value from that 1 cell and not the range.
What I mean by "Active Cell" and "Active Range":
What I know so far:
sheet.getActiveRangeList() works as intended and returns the entire "Active Range" of cells. I know this since I am able to call a .clear on this and see the selected range of cells clear out!
sheet.getActiveRangeList().getRanges() fails -> this is supposed to return an array of Range objects: Range[], which it does, but the array only has the "Active Cell"
A contiguous selection is a single Range. Use Range#getValues instead, to access all the values of a given Range. Range#getValue() will always return only the top-left value, even if there is more than 1 cell in the Range.
function previewModal() {
const sheet = SpreadsheetApp.getActiveSpreadsheet(),
ranges = sheet.getActiveRangeList().getRanges(),
htmlPrefix = "",
html = ranges.reduce(function (rgSummary, rg) {
var summary = rg
.getValues() // 2D array
.reduce(arrayToHtmlReduceCallback_, "");
return rgSummary + summary;
}, htmlPrefix);
const output = HtmlService.createHtmlOutput(html).setWidth(1000).setHeight(1000);
SpreadsheetApp.getUi().showModalDialog(output, 'Preview');
}
function arrayToHtmlReduceCallback_(acc, row, i, allArrayRows) {
const rowSummary = row[0]; // Using only the content of the first column.
return acc + '<div>' + rowSummary + '</div><hr>';
}
Additional Reading:
Range#getValue()
Range#getValues()
Array#reduce

How to automatically copy/paste data using App Script in Google Sheets

I'm trying to write some code using App Scripts that will (via a daily trigger), copy/paste data from the cells F13:G13 to the first empty cell in column I. Here is my code:
function TrackCurrentValues()
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getSheets()[0];
var lastRow = getLastRowInColumn(sheet, 'I:I');
// Logger.log('I' + parseInt(lastRow + 1));
var pasteRange = sheet.getRange('I' + parseInt(lastRow + 1) );
pasteRange.activate();
// now that we have the first empty cell in column I, paste the values we found from cells F13:G13
spreadsheet.getRange('F13:G13').copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
};
function getLastRowInColumn(sheetObj, range) {
return sheetObj.getRange(range)
.getValues().filter (String).length + 1
}
When it runs, what happens is that the data is copied from the proper location, but it's always pasted to cell A1. Moreover, the number pasted appears is prefixed with the British pound sterling character (£).
What could be wrong? It (usually) works if I run it manually. The main thing is that it doesn't find the empty cell in I:I.
This code should do what you're looking for:
function TrackCurrentValues(){
var sheet = SpreadsheetApp.getActive().getSheets()[0];
var lastRow = getLastRowInColumn(sheet, 'I:I');
var pasteRange = sheet.getRange(lastRow + 1,9,1,2);
var copyRange = sheet.getRange(13,6,1,2)
pasteRange.setValues(copyRange.getValues())
};
function getLastRowInColumn(sheetObj, range) {
return sheetObj.getRange(range).getValues().filter(String).length
}
On your £ chracter question, that range is likely formatted to display that currency symbol. To update this, select the range, go to the toolbar and select Format > Number > Specify the format you would like
Additional Thoughts:
i) You are adding one to lastRow variable twice (once in getLastRowInCOlumn function and again in pasteRange definition)
ii) I would reocmmend not using "active ranges" to store a location, instead store that range in a variable
iii) It seems your copy range was 2 columns wide but your pasteRange was only 1 column wide

replaceText wildcard in Google Sheet script

I've been trying to work on replacing some text within a Google script but it's not producing what I'd like. At present I am using Cameron Roberts' script from here - How do I replace text in a spreadsheet with Google Apps Script? to make my replacements however I've not got it quite right.
Using that code I am trying to replace the word 'values' with '1. values' however if I run the code multiple times it produces '1. 1. values' etc as it just finds the 'values' string. What I'd like is a wildcard which just searches for 'values' and then puts '1. values' in but I can't seem to grasp the regular expression syntax well enough to fix it.
function testReplaceInSheet(){
var sheet = SpreadsheetApp.getActiveSheet()
replaceInSheet(sheet,'values','1. values');
}
function replaceInSheet(sheet, to_replace, replace_with) {
//get the current data range values as an array
var values = sheet.getDataRange().getValues();
//loop over the rows in the array
for(var row in values){
//use Array.map to execute a replace call on each of the cells in the row.
var replaced_values = values[row].map(function(original_value){
return original_value.toString().replace(to_replace,replace_with);
});
//replace the original row values with the replaced values
values[row] = replaced_values;
}
//write the updated values to the sheet
sheet.getDataRange().setValues(values);
}
If a small change in formatting is acceptable to you then you can capitalize one of the 'values', ex:
replaceInSheet(sheet,'values','1. Values');
This adds a prefix if the prefix doesn't already exist.
Simulates a negative look behind that's why the reversing is going on
function addPrefix(s, find, prefix) {
function reverse(x) {return x.split("").reverse().join("");}
var sr = reverse(s);
var findr = reverse(find);
var prefixr = reverse(prefix);
var findRegexpr = new RegExp(findr + "(?!" + prefixr + ")");
return reverse(sr.replace(findRegexpr, findr + prefixr))
}