replaceText wildcard in Google Sheet script - google-apps-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))
}

Related

Apps Script: Check if range is empty and copy data

I'm trying to build an automated list with google sheets. The first sheet(A) is for input of production data of a week. The second sheet(B) should be the data archive. Thus i want the content from sheet A copied to sheet B and then deleted in sheet a. It should be copied in the next empty range in sheet B.
My problem must be inside the notation of the "while" or / and the "if" but nothing seems to work properly.
The while checks if sheet A is already emptied, if not the "if" function checks a specific range in sheet B if it is empty. If thats the case it should be copying the data and then delete it. Else the column of the range in sheet b is changed to the next range (spaltennummer + 6).
While troubleshooting it either stays in the while (finds no empty range?) or it runs through without any effect. I tried "== 0", "== """, isblank and so on. (every option available?). Google didnt seem able to provide me an answer...
Thanks for ur help.
Code:
function myFunction() {}
function leerzelle(){
var spaltennummer = 4;
var rangeDatenarchiv = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tabellenblatt1").getRange(8,spaltennummer,15,5);
var rangeDateneingabe = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Dateneingabe").getRange("J8:N22");
var values = rangeDateneingabe.getValues();
while(rangeDateneingabe !== 0){
if(rangeDatenarchiv == 0) {
rangeDatenarchiv.setValues(values);
rangeDateneingabe.setValues("");
}
else{
spaltennummer = spaltennummer + 6;
}
}
}
Try this:
Code:
function leerzelle() {
var spaltennummer = 4;
// Data entry
var rangeDateneingabe = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Dateneingabe").getRange("J8:N22");
var values = rangeDateneingabe.getValues();
// While we have value in data entry sheet
while (!rangeDateneingabe.isBlank()) {
// Update archive everytime you increment in else
var rangeDatenarchiv = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tabellenblatt1").getRange(8, spaltennummer, 15, 5);
// If archive range is blank, move values
if (rangeDatenarchiv.isBlank()) {
rangeDatenarchiv.setValues(values);
rangeDateneingabe.clearContent();
}
else {
spaltennummer = spaltennummer + 6;
}
}
}
Note:
You just can't compare a range to a number, use isBlank to check if it doesn't have values instead.
You can't use setValues("") on a range to remove the contents, use clearContent instead to delete those values.
You need to redeclare archive range everytime you loop using the incremented column number
References:
isBlank
clearContent

Creating New Tabs Based on a List of Names in Google Sheets

I am trying to create a spreadsheet which has a 'Class List' and 'Template' sheet. Once names are added to the 'Class List' (C5:C) I would like to generate a new tab for each name based on the 'Template' sheet. The names of these new tabs will reflect the names in the list. The class list will be constantly changing and being added to.
This is the code I am currently using and it works perfectly, however, it always generates 1x blank, un-named tab at the end (See picture below). Is there a way to stop this from happening?
function newSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var templateSheet = ss.getSheetByName("Template");
var sheet1 = ss.getSheetByName("Class List")
var getNames = sheet1.getRange("C5:C").getValues().filter(String).toString().split(",");
for (var i = 0; i < getNames.length; i++) {
var copy = ss.getSheetByName(getNames[i]);
if (copy) {
Logger.log("Sheet already exists");
} else {
templateSheet.copyTo(ss).setName(getNames[i]);
ss.setActiveSheet(ss.getSheetByName(getNames[i]));
ss.moveActiveSheet(ss.getNumSheets());
}
}
Issue:
Many cells in your column C look empty but in fact contain whitespaces (" "), so these cell values are not getting filtered out by doing .filter(String). Because of this, a sheet named " " is created.
Solution:
Use trim() to remove whitespaces. For example, you could do this:
getNames = getNames.filter(value => value.trim() !== "");
Note:
I'm not sure why you are using toString() and split(). If your purpose is to transform the 2D array to a simple array (that is, an array in which each element is a string, not another array) you could use flat() instead.
You could do something like this instead:
var getNames = sheet1.getRange("C5:C").getValues().flat().filter(value => {
return value.trim() !== ""
})

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

Google Sheets App Script - Get values from formula instead of writing to cell

My script sets the values of a whole sheet by using an arrayformula.
sheet.getRange("A2").setFormula("=ArrayFormula(UNIQUE(FILTER(Furniture!$A$2:$S$15000,ISNA(MATCH(Furniture!$A$2:$A$15000,Shopify!$F$2:$F$50000,0)))))");
Is it possible to get these values as an array, without updating the sheet? At the moment, the best I can think of is getting the values from the sheet after applying the above formula. There must be a more efficient way.
You want to get the values from 'Furniture' sheet that aren't included in the values from 'Shopify' sheet, and then, delete the duplicate values from the result. You can get the values for each sheet range using getRange() [1] function combined with getLastRow() [2] function in order to get the range up to the last row with data (You can change this and simply use your entire A1 notation for each sheet). You can use JS filter function [3] to filter the values from Furniture that aren't in Shopify, and finally reduce function [4] to get rid of the duplicate values. Below is the working code, you need to replace the Spreadsheet ID and run getUniqueValues function from the editor, this will log you the resultant array.
function getUniqueValues() {
var ss = SpreadsheetApp.openById('[SPREADSHEET-ID]');
var furnitureSheet = ss.getSheetByName('Furniture');
var shopifySheet = ss.getSheetByName('Shopify');
//Get the range for each sheet based on last row with data
var furnitureArray = furnitureSheet.getRange('A2:A' + furnitureSheet.getLastRow()).getValues().join().split(",");
var shopifyArray = shopifySheet.getRange('F2:F' + shopifySheet.getLastRow()).getValues().join().split(",");
var resultArray = furnitureArray.filter(function(furnitureValue) {
if(shopifyArray.indexOf(furnitureValue) == -1) {
return true;
}
return false
});
resultArray = resultArray.reduce(function(a,b){
if (a.indexOf(b) < 0 ) a.push(b);
return a;
},[]);
Logger.log(resultArray);
}
[1] https://developers.google.com/apps-script/reference/spreadsheet/sheet#getrangea1notation
[2] https://developers.google.com/apps-script/reference/spreadsheet/sheet#getlastrow
[3] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
[4] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce

How can I avoid a VLOOKUP Error that occurs when I delete a name?

The pictures used are only from an example sheet! My basic problem is that I have a list called Assignment in which names appear (dropdown list). For Location (in the assignment sheet) I use the following formula: =IF(C2<>"",VLOOKUP(C2,'Input Data'!C$3:D$7,2,FALSE),"")
These names are assigned certain values, they are in the same line. The names are defined in a worksheet called Input Data!
If I now delete a name like Green, John from the Input Data worksheet, then I get the following error in another worksheet (Evaluation). (More than 40 people have access to this worksheet and randomly delete names)In this evaluation worksheet the values are evaluated by the following formula:
=ARRAY_CONSTRAIN(ARRAYFORMULA(SUM(IF((IF($B$2="dontcare",1,REGEXMATCH(Assignment!$E$3:$E$577,$B$2 &"*")))*(IF($B$3="dontcare",1,(Assignment!$E$3:$E$577=$B$3)))*(IF($B$4="dontcare",1,(Assignment!$D$3:$D$577=$B$4)))*(IF($B$5="dontcare",1,(Assignment!$F$3:$F$577=$B$5)))*(IF($B$6="dontcare",1,(Assignment!$B$3:$B$577=$B$6))),(Assignment!S$3:S$577)))), 1, 1)
The following error appears in the evaluation sheet:
Error:
During the evaluation of VLOOKUP the value "Green, John" was not found.
How can I avoid this error? Is it possible to avoid this error with a macro that deletes Names from assignment sheet that are not in the Input data sheet? Do you have any ideas for a code?Maybe a Formula or perhaps a Macro?
example sheet with explanation: https://docs.google.com/spreadsheets/d/1OU_95Lhf6p0ju2TLlz8xmTegHpzTYu4DW0_X57mObBc/edit#gid=1763280488
If what you want to do is make sure that rows are deleted in a sheet when there are incorrect values you could try something like this in Apps Script:
function onEdit(e) {
var spreadsheet = e.source;
var assignment = spreadsheet.getSheetByName("Assignment");
var assignmentRange = assignment.getDataRange();
var assignmentNames = assignment.getRange(3, 2, assignmentRange.getNumRows());
var inputData = spreadsheet.getSheetByName("Input Data");
var inputDataRange = inputData.getDataRange();
var i = 1;
while(assignmentNames.getNumRows() > i){
var currentCell = assignmentNames.getCell(i, 1);
var txtFinder = inputDataRange.createTextFinder(currentCell.getValue());
txtFinder.matchEntireCell(true);
if(!txtFinder.findNext()){
assignment.deleteRow(currentCell.getRow())
}else{
// We are only steping when no elements have been deleted
// Otherwise we would skip rows due to shifting in row deletion
i++;
}
}
}
Explanation
onEdit is a special function name in Apps Script that would execute every time it's parent sheet is modified.
After that we retrieve the spreadsheet from the event object
var spreadsheet = e.source;
Now we get the relevant range in the Assignment sheet. Look at the usage of getDataRange documentation to avoid retrieving unnecessary cell values. And from that range we actually get the specific column we are interested on.
var assignment = spreadsheet.getSheetByName("Assignment");
var assignmentRange = assignment.getDataRange();
var assignmentNames = assignment.getRange(3, 2, assignmentRange.getNumRows());
Now we do the same for the other sheet(Input Data):
var inputData = spreadsheet.getSheetByName("Input Data");
var inputDataRange = inputData.getDataRange();
Note: Here I'm not getting a specified column because I assume that the full name will not repeat in any other column. But if you want you could get the specified range as I have done at Assignment.
After that we want to look for specific values in the Assignment range that don't exist in the Input Data sheet, you should try the TextFinder.
For every name in Assignment you should create a TextFinder. I have also forced to make a whole cell match.
var i = 1;
while(assignmentNames.getNumRows() > i){
var currentCell = assignmentNames.getCell(i, 1);
var txtFinder = inputDataRange.createTextFinder(currentCell.getValue());
txtFinder.matchEntireCell(true);
If txtFinder finds a value the findNext() will evaluate to true. In the other hand when the txtFinder does not find a value it will be null and evaluated to false.
if(!txtFinder.findNext()){
assignment.deleteRow(currentCell.getRow())
}else{
// We are only stepping forward when no elements have been deleted
// Otherwise we would skip rows due to shifting in row deletion
i++;
}
}
}