Some of you may be familiar with content grouping in Google Analytics, which basically lets you group any number of URL's in user-specified groups (this is useful for analyzing pages that belong together all at the same time). I'm working on a script to take that to the next level and use it in Google Sheets as well.
Goal: have a working script that rewrites URL's and gives them another name, regardless of whether it uses upper or lower cases in the URL.
So far I have this:
function onOpen() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange("a1:a10000");
var to_replace = /.*example.*/;
var replace_with = "TEST";
var to_replace2 = /.*another-example.*/;
var replace_with2 = "TEST-Nr2";
replaceInSheet(sheet,range, to_replace, replace_with);
replaceInSheet(sheet,range, to_replace2, replace_with2);
}
This script works in the sense that it rewrites URL's with 'Example' in it to 'Test' and it rewrites 'Another-example' into TEST-Nr2.
However, the final script will probably have thousands of URL's that will need to be rewritten. Furthermore, some URL's have uppercases in them, which I want to ignore and just rewrite.
All of the above leads me to two questions:
How can I write the script in such a way (with regular expressions for example?) that I won't have a Googleplex number of To_replace's and replace_with's?
How can I make my to_replace variables case-incensitive?
If any more information is needed on this matter I will gladly provide so.
Kind regards,
JNeu
Somehow you know the patterns and the replacement values, yes? You need to impart that knowledge to your script.
The simplest way is to read it from a spreadsheet, e.g. on some sheet in some workbook, you have 1 column with the pattern, and another column with the replacement. Then you just read that data in (Range#getValues()), and then iterate that array to process your data range. Note that the pattern you store in the sheet should not include the literal constructor slashes, i.e. you'd want \d{1,3} and not /\d{1,3}/ in the cell.
Example:
function processAll() {
const source = SpreadsheetApp.openById("id of the spreadsheet with pattern - replacement data"),
info = source.getSheetByName("some sheet name")
.getDataRange().getValues();
const databook = SpreadsheetApp.getActive(),
sheet = databook.getSheetByName("name of the sheet with data to process");
if (!sheet) return; // sheet with that name doesn't exist.
const range = sheet.getRange(1, 1, sheet.getLastRow(), 1);
info.forEach(function (row) {
// Create case-insensitive pattern from the string in Column A, e.g. \d{1,3} and NOT /\d{1,3}/
var pattern = new RegExp(row[0], "i");
var repl = row[1]; // replacement text from Column B
replaceInSheet(sheet, range, pattern, repl);
});
}
Additional Reading:
RegExp
Array#forEach
Related
I am working on a Google sheet script to manage stocks of items in a game, which is supposed to work as such:
People can make request to deposite or withdraw items using a Google form, which send all the infos, including what resource and in what amount, to a first "log" sheet. I then want a script to read these logs, and use them to update a different sheet, which show the actual stocks.
I should mention, there's about 800 different items to stock, and we like to move them around (up or down the list) because we're dumb.
So my idea what the have the script first retrieve the name of the item we made a request for, then try to match it in the stock sheet.
If it can, it should then add or substract the amount to the stock.
If it can't, it should just colour the log line in red so we can see it and redo the request.
My first problem is that I have no idea if a script in Gsheet can stay active for a long time, and the second is that I have even less of an idea how to properly retrieve a string of text and store it, then compare it with others, and that +800 times each time.
Thank you !
From the question
My first problem is that I have no idea if a script in Gsheet can stay active for a long time,
Google Apps Script have quotas. In this case, the corresponding quota is the execution time limit. For free accounts the limit is 6 minutes, for Workspace accounts the limit is 30 minutes.
and the second is that I have even less of an idea how to properly retrieve a string of text and store it, then compare it with others, and that +800 times each time.
Start by reading https://developers.google.com/apps-script/guides/sheets
Tl;Dr.
You need to learn the pretty basics of JavaScript.
You might use the Spreadsheet Service (Class SpreadsheetApp) or the Advanced Sheets Service, i.e.
/**
* Returns the values from the data range of the active sheet
*
*/
function readData(){
const sheet = spreadsheet.getActiveSheet();
const values = sheet.getDataRange().getValues();
return values;
}
You should decide where do you will store the values, then use JavaScript comparison expressions. You might use loops (for, while, do..while, or use Array methods like Array.prototype.forEach()
Here is an example how it could be done for simplest case, for manual firing of the functions.
Let's say you have the log sheet that look like this:
And your data sheet looks like this:
Here is the function that takes all items from the log sheet, sums them and put on the data sheet:
function add_all_items_from_log() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var log = ss.getSheetByName('log').getDataRange().getValues();
// put all data into the object {item1:q, item2:q, item3:q, ...etc}
var obj = {};
for (let [date, item, q] of log) {
if (item in obj) obj[item] += q; else obj[item] = q;
}
console.log(obj);
// convert the object into a 2d array [[item1,q], [item2,q], [item3,q], ...]
var array = Object.keys(obj).map(key => [key, obj[key]]);
console.log(array);
// put the array on the data sheet (starting from second row)
var sheet = ss.getSheetByName('data');
sheet.getRange(2,1,sheet.getLastRow()).clearContent();
sheet.getRange(2,1,array.length, array[0].length).setValues(array);
}
The result:
Here is the function that takes item from the last line of the log sheet and add the item to the data sheet:
function add_last_item_from_log() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// get item from the last row of the log sheet
var [date, item, q] = ss.getSheetByName('log').getDataRange().getValues().pop();
console.log(date, item, q);
// get data from the data sheet
var sheet = ss.getSheetByName('data');
var [header, ...data] = sheet.getDataRange().getValues();
// put the data into the object {item1:q, item2:q, item3:q, ...etc}
var obj = {};
data.forEach(row => obj[row[0]] = row[1]);
console.log(obj);
// add the item to the object
if (item in obj) obj[item] += q; else obj[item] = q;
// convert the object into a 2d array [[item1,q], [item2,q], [item3,q], ...]
var array = Object.keys(obj).map(key => [key, obj[key]]);
console.log(array);
// put the array on the sheet (starting from second row)
var sheet = ss.getSheetByName('data');
sheet.getRange(2,1,sheet.getLastRow()).clearContent();
sheet.getRange(2,1,array.length, array[0].length).setValues(array);
}
Here is my sheet.
You can run these function manually from Text Editor. Just to see how it works. But actually, as far as I can tell, you better to run the last function (or its variant) automatically every time the log sheet is updated from the Form submit. It can be done with the trigger onFormSubmit().
And this is a simplest case. If you have 800+ items and many columns the code may require some optimizations.
I currently have a script that looks into multiple spreadsheets and pulls in data from a sheet contained within depending on user input.
Unfortunately there are times when users add new sheets in and do not follow the correct format for example each sheet should have four numbers, however sometimes a user will hit the space-bar before inputting those numbers.
Then when the script runs it will come back with an error because it can not find the sheet its looking for, so I need to build in a OR function, my code is quite long so I will type out an example of where I need the function below.
But for example if we can not find sheet "1234" I need to look for sheet " 1234"
var ss= getActiveSpreadsheet()
var sheet = '1234';
var altSheet = ' 1234'
var s1 = ss.getSheetByName(sheet)
//if sheet can not be found, I need to find altSheet instead
You can get the full list of sheets using getSheets(), then use the find() and trim() methods to find the correct one.
const s1 = ss.getSheets().find(sheet => sheet.getName().trim() === '1234');
I have a spreadsheet with many formula referencing named ranges.
The spreadsheet has a script associated with it in a custom menu that imports updated data which can increase the length of the data needed in each range. Once the data are imported the script updates the named ranges to be the length of the new data.
Here is the code block that imports new data and then updates the named ranges:
// add the CSV menu. Might change this to be an automatic update base don date
function onOpen() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var csvMenuEntries = [{name: "Update Data", functionName: "importFromCSV"}];
ss.addMenu("Update", csvMenuEntries);
}
function importFromCSV() {
var file = DriveApp.getFilesByName("double_leads_data.csv");// get the file object
var csvFile = file.next().getBlob().getDataAsString();// get string content
var csvData = Utilities.parseCsv(csvFile);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('DataImport'); // only add data to the tab DataImport to prevent overwriting other parts of the spreadsheet
sheet.getRange(2,1, csvData.length, csvData[0].length).setValues(csvData);// write to sheet in one single step. Start at row 2 (getRange(2... )
SpreadsheetApp.getUi().alert('Data Updated');
//now update the named ranges if they have changed in length
var openEnded = ["business_unit", "channel", "date", "leads", "medium", "region" ];
for(i in openEnded) {
var r = ss.getRangeByName(openEnded[i]);
var rlr = r.getLastRow();
var s = r.getSheet();
var slr = s.getMaxRows();
if(rlr==slr ) continue; // ok as is-skip to next name
var rfr = r.getRow();
var rfc = r.getColumn();
var rnc = r.getNumColumns();
var rnr = slr - rfr + 1;
ss.removeNamedRange(openEnded[i]);
ss.setNamedRange( openEnded[i], s.getRange(rfr, rfc, rnr, rnc ));
}
}
All works well - the data import and the named ranges update. However, after the update all the formula referencing the named ranges break and show #REF where they previously referenced the corresponding named range.
Reading some documentation here there is a sentence
When you delete a named range, any formulas referencing this named
range will no longer work. However, protected ranges that reference a
named range will swap out the named range for the cell values
themselves and continue to work.
I'm not really sure what that means. If I use a protected range instead will it all work? I tried editing the code above? I read about getProtections() here so tried making a small edit:
var openEnded = ["businessunit2", "date2", "leads2", "medium2", "region2", "saleschannel2" ];
var openEnded = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE)
I didn't really expect this to work but was worth a try.
Is there a solution here? How can I update a named range with a script without breaking existing formula which references those ranges? Will using getProtections() lead to a solution or is that just a diversion?
Use INDIRECT("rangeName") in formulas instead of just rangeName. The only way to extend the range programmatically is by removing it and then adding it back with a new definition. This process breaks the formula and returns #ref instead of the range name.
=sum(indirect("test1"),indirect("test3"))
This is a messy and should be unnecessary workaround. If you agree please star the item in the issue tracker.
https://code.google.com/p/google-apps-script-issues/issues/detail?id=5048
the detail you mention about protected ranges wont help you. That refers to the protected range definition in their details pane. You can define a protected range to be a named range and if the named range is deleted it won't break the protected range definition.i also ran into this a while ago and consider it to be a serious bug in their named ranges api. Its ridiculous that their api doesnt have a way to modify them (instead of deleting and recreating). I mean obviously if we use named ranges is because we expect them to change. Sorry for the rant but this is a very old issue that is still broken.
edit: see
https://code.google.com/p/google-apps-script-issues/issues/detail?id=1040 (i'm #7 there from 1.5 years ago)
and
https://code.google.com/p/gdata-java-client/issues/detail?id=196 (im #4 and just added #5 there)
Please star both.
formulas that reference the NamedRange are being broken by the line
ss.removeNamedRange(openEnded[i]);
I believe you can simply omit this line, and go directly to
ss.setNamedRange( openEnded[i], s.getRange(rfr, rfc, rnr, rnc ));
this approach appears to be working for me in a GAS script that adds a column to a NamedRange in Google Sheets. Formulas in other cells reference this named range and are not broken when my script executes
I read the three issue tracker postings and I understand the concern is generating duplicate entries in the set of named ranges. So far I have not seen this behavior so perhaps this bug was fixed.
The following code will update a named range without deleting it or create the range if it doesn't exist.
function fixNamedRange (ss, name, range) {
var ssNamedRanges = ss.getNamedRanges();
var ssRangeNames = ssNamedRanges.map (function (ssRange) {
return ssRange.getName();
}
);
var myRange = ssNamedRanges[ssRangeNames.indexOf(name)];
if (myRange) {
return myRange.setRange(range);
} else {
ss.setNamedRange(name, range);
return -1;
}
}
Edited to employ .map method instead of iterating through array of named ranges.
I have an array of a couple (the array is up to 10) date/time that I want to write to a spreadsheet using getRange().setValues(). I'm converting the array to a string and it looks correct in Logger.
[Mon Feb 02 14:01:00 GMT-06:00 2015, Tue Feb 02 01:00:00 GMT-06:00 2016, , , , , , , , ]
When I try to write the string to a single cell in a sheet:
target6.setValues(source_range6_values);
I get this error:
Incorrect range width, was 10 but should be 1 (line 84, file "Code")
Edited 4/28/2014 adding entire script:
/**
* Copies source range and pastes at first empty row on target sheet
*/
function CopyIt(){
//Establishing source and target sheets
var source_spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var target_spreadsheet = SpreadsheetApp.openById("0AhCv9Xu_eRnSdHpLTkc0d1ZURUtyTU9oRjdFbmpMUFE");
// Get source and target sheets - can be the same or different
var sourcesheet = source_spreadsheet.getSheetByName("Form Responses");
var targetsheet = target_spreadsheet.getSheetByName("Work_Orders");
//Get row of last form submission
var source_last_row = sourcesheet.getLastRow();
// Check for answer to Do you need a Flyer Created? If No, end now. If Yes, continue.
var check = sourcesheet.getRange("T"+(source_last_row)).getValue();
if (check == 'Yes') {
//Pulling date(s) from the users form entry (source sheet) into an array
var daterange = sourcesheet.getRange("H"+source_last_row+":Q"+source_last_row);
//Getting the values of the array
var classDate = daterange.getValues();
//changing the array values to a string
classDate.toString();
//Building a new variable with the string to be inserted below in the target sheet
var source_range6_values = classDate;
//source_range6_values.toString();
Logger.log(classDate[0]);
// Get the last row on the target sheet
var last_row = targetsheet.getLastRow();
//Setting the target cell in the Marketing Work Order sheet
var target6 = targetsheet.getRange("U"+(last_row+1));
// Aadding a new row in the target sheet
targetsheet.insertRowAfter(last_row);
//Inserting the values of source_range6_values into the target sheet. Unfortunately it does not enter the data into the same field and it's in military time.
target6.setValue(source_range6_values);
Logger.log(source_range6_values);
}
}
To give a correct answer for your question, i guess i need to know how you get the value of source_range6_values.
One quick guess is you might want to use target6.setValue instead of target6.setValues since you want to write the data into one cell only...
A quick & dirty way would be to replace the commas(with spaces):
source = String(source_range6_values).replace("," , " ");
I've had fun with GAS and variables. Casting it as a String should let you use the string functions on it. If that doesn't work can you share a mock-up of your sheets so I can take a look?
edit:
I had to play around with it a bit, seems google's version of .replace() only replaces the first instance (and doesn't allow .replaceAll() ).
I edited your code starting on line 23:
//Getting the values of the array
var classDate = daterange.getValues().toString();
//Building a new variable with the string to be inserted below in the target sheet
//Google has bugs, .replace() seems to only replace the first instance
//-while {} loop replaces all of them
while (!classDate.equals(classDate.replace("," , " "))) { classDate = classDate.replace("," , " "); };
var source_range6_values = classDate;
All the dates are in one cell if you change only those lines (and no errors).
I appreciate the help you two have given me trying to answer this question. #swimmingwood fixed the actual capture of the data into a string, but it left commas and when I inserted it into the target sheet, it wrote it to multiple cells with an error. It did write to the sheet but the error had you use a CTRL-E (inside the taget sheet) to complete the insert and wrote them into separate cells.
#MickATX suggested the code to replace the commas in the string with a space, which would be fine, but apparently he discovered a Google scripting problem that would only allow for the first comma to be replaced and ignore the rest. Great knowledge never-the-less.
I ended up using a formula in an addition cell in the source sheet that looked like this:
=ArrayFormula(CONCATENATE(REPT(TEXT(H2:Q2,"mm/dd/yyyy hh:mm a")&CHAR(10),H2:Q2>0)))
This formula wrote all the date/time entries provided by the form entry into one cell of the source sheet and ONLY the number of entries (1-10). I then wrote that single cell to the target sheet via the script.
Thanks to #swimmingwood and #MickATX for trying to help me, both provided worthy knowledge.
I've read a couple of strange answers here...
If you write an 2D array to a sheet it will obviously be written accross multiple cells... commas are definitely not the issue but the nature of the object is.
Simply convert your array into a string using .toString() or .join() (the latter providing the advantage you can choose the separator to use) and setValue() (without S) at the place you want.
the commas you see in the logger are only typographic representation of array elements separators...
And, last point : the .join() or .toString() methods return new variables, they don't modify the original value so when you write classDate.toString(); you are not doing anything ...
you should write it like this :
classDateAsAString = classDate.toString();
finally your code :
function CopyIt(){
//Establishing source and target sheets
var source_spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var target_spreadsheet = SpreadsheetApp.openById("0AhCv9Xu_eRnSdHpLTkc0d1ZURUtyTU9oRjdFbmpMUFE");
// Get source and target sheets - can be the same or different
var sourcesheet = source_spreadsheet.getSheetByName("Form Responses");
var targetsheet = target_spreadsheet.getSheetByName("Work_Orders");
//Get row of last form submission
var source_last_row = sourcesheet.getLastRow();
// Check for answer to Do you need a Flyer Created? If No, end now. If Yes, continue.
var check = sourcesheet.getRange("T"+(source_last_row)).getValue();
if (check == 'Yes') {
//Pulling date(s) from the users form entry (source sheet) into an array
var daterange = sourcesheet.getRange("H"+source_last_row+":Q"+source_last_row);
//Getting the values of the array
var classDate = daterange.getValues();
var source_range6_values = classDate.join(' & ');// using & as separator for example
// Get the last row on the target sheet
var last_row = targetsheet.getLastRow();
//Setting the target cell in the Marketing Work Order sheet
var target6 = targetsheet.getRange("U"+(last_row+1));
// Adding a new row in the target sheet
targetsheet.insertRowAfter(last_row);
//Inserting the values of source_range6_values into the target sheet. Unfortunately it does not enter the data into the same field and it's in military time.
target6.setValue(source_range6_values);
Logger.log(source_range6_values);
}
}
Now if you want to format the dates in a more civilized way, that should be handled a bit differently... let me know if you still need it / want it.
I have many spreadsheets that get filled out weekly and closed at the start of the new week. I've written a script to go through and clear out all kinds of ranges in a lot of different sheets. See code below. My question is there a better way to do this instead of having to clear ranges one section at a time and have a million clearContent functions? I cannot figure out how to write a function to clearContent where I can list many ranges all in the same function.
function startWeek() {
var confirm = Browser.msgBox('Did you **Close the Week** first?','Pressing YES will clear your week', Browser.Buttons.YES_NO);
if(confirm=='no'){Logger.log('The user clicked "NO."')};
if(confirm=='yes'){
var sheet = SpreadsheetApp.getActive().getSheetByName('INVOICE LOG');
sheet.getRange('A3:M47').clearContent();
var sheet = SpreadsheetApp.getActive().getSheetByName('DAILY INVENTORY');
sheet.getRange('C5:C8').clearContent();
sheet.getRange('D6:I8').clearContent();
sheet.getRange('C10:I10').clearContent();
sheet.getRange('C13:C16').clearContent();
sheet.getRange('D14:I16').clearContent();
sheet.getRange('C18:I18').clearContent();
sheet.getRange('C21:C24').clearContent();
sheet.getRange('D22:I24').clearContent();
sheet.getRange('C26:I26').clearContent();
sheet.getRange('C29:C32').clearContent();
sheet.getRange('D30:I32').clearContent();
sheet.getRange('C34:I34').clearContent();
sheet.getRange('C37:C40').clearContent();
sheet.getRange('D38:I40').clearContent();
sheet.getRange('C42:I42').clearContent();
sheet.getRange('C45:C48').clearContent();
sheet.getRange('D46:I48').clearContent();
sheet.getRange('C50:I50').clearContent();
sheet.getRange('C55:C58').clearContent();
sheet.getRange('D56:I58').clearContent();
sheet.getRange('C60:I60').clearContent();
sheet.getRange('C63:C66').clearContent();
sheet.getRange('D64:I66').clearContent();
sheet.getRange('C68:I68').clearContent();
sheet.getRange('C71:C74').clearContent();
sheet.getRange('D72:I74').clearContent();
sheet.getRange('C76:I76').clearContent();
sheet.getRange('C79:C82').clearContent();
sheet.getRange('D80:I82').clearContent();
sheet.getRange('C84:I84').clearContent();
sheet.getRange('C87:C90').clearContent();
sheet.getRange('D88:I90').clearContent();
sheet.getRange('C92:I92').clearContent();
sheet.getRange('C95:C98').clearContent();
sheet.getRange('D96:I98').clearContent();
sheet.getRange('C100:I100').clearContent();
sheet.getRange('C105:C108').clearContent();
sheet.getRange('D106:I108').clearContent();
sheet.getRange('C110:I110').clearContent();
sheet.getRange('C113:C116').clearContent();
sheet.getRange('D114:I116').clearContent();
sheet.getRange('C118:I118').clearContent();
sheet.getRange('C121:C124').clearContent();
sheet.getRange('D122:I124').clearContent();
sheet.getRange('C126:I126').clearContent();
sheet.getRange('C129:C132').clearContent();
sheet.getRange('D130:I132').clearContent();
sheet.getRange('C134:I134').clearContent();
sheet.getRange('C137:C140').clearContent();
sheet.getRange('D138:I140').clearContent();
sheet.getRange('C142:I142').clearContent();
sheet.getRange('C145:C148').clearContent();
sheet.getRange('D146:I148').clearContent();
sheet.getRange('C150:I150').clearContent();
var sheet = SpreadsheetApp.getActive().getSheetByName('FOOD INVENTORY');
sheet.getRange('D5:F615').clearContent();
var sheet = SpreadsheetApp.getActive().getSheetByName('LIQUOR INVENTORY');
sheet.getRange('D6:G361').clearContent();
sheet.getRange('E365:G520').clearContent();
sheet.getRange('D524:G573').clearContent();
var sheet = SpreadsheetApp.getActive().getSheetByName('DAILY SALES SHEET');
sheet.getRange('B4:H10').clearContent();
sheet.getRange('B12:H12').clearContent();
sheet.getRange('B14:H20').clearContent();
sheet.getRange('B22:H27').clearContent();
sheet.getRange('B29:H30').clearContent();
sheet.getRange('B33:H34').clearContent();
sheet.getRange('B36:H38').clearContent();
sheet.getRange('B43:H44').clearContent();
var sheet = SpreadsheetApp.getActive().getSheetByName('LAST WEEK INVENTORY');
sheet.getRange('E3:E9').clearContent();
var sheet = SpreadsheetApp.getActive().getSheetByName('SAFE AUDIT');
sheet.getRange('C3:P11').clearContent();
sheet.getRange('C14:P18').clearContent();
sheet.getRange('C22:P22').clearContent();
var destination = SpreadsheetApp.getActiveSpreadsheet();
var name = Browser.inputBox('New Week', 'Enter Pub Name & WE Date (ex. SandwichWE02-02-14)', Browser.Buttons.OK);
destination.rename(name)
};
}
Unfortunately there is not currently a Apps Script method to clear multiple ranges with a single function call.
What you might find a little easier to manage is to instead define one or more data structures (even a simple array) containing the Ranges in question. Then you can clear them all by simply looping over the data structure and calling clearContent() on each Range. This would separate the work of keeping track of your Ranges from the work of clearing them. The data structure might also be useful in other areas of your code.
For better organization, you might also make use of the Spreadsheet.setNamedRange() and Spreadsheet.getNamedRange() functions to assign simple IDs to your Ranges. Note that you cannot give more than one Range the same name.