Iterate over sheet using GS while there is any data in columns - google-apps-script

I need to do a small script, its basic idea is to have a sheet with names in one columns and some random info in another (to the right). The sheet has some count (unknown in advance) of such records, so they are like
John 39483984
George 3498349
Layla 23948
So that a user can enter any number of such simple records, my script must create a file for each name (file is of the same name) and write the number into that file. I managed to find how to create files (though still couldn't find out how to create file in the current folder, same as where the sheet is located - this is just a side question, but if you know how to do it, please tell me). The only real problem is iterating through the records. My idea was to go through them one by one and stop when there is an empty record - basic strategy, but I wasn't able to find how to implement it (yep!). There are range functions, but there I should know in advance the range; also there is a function to get selected cells but that will require a user to select the records, which is strange.
So please suggest me a solution if it exists in this frustration Google Script.

function createFilesForEachNameInSheet() {
// First, you connect to the spreadsheet, and store the connection into a variable
var ss = SpreadsheetApp.openById("SPREADSHEET_KEY_GOES_HERE"); // you do know how to get the spreadsheet key, right?
// Then, you take the sheet from that spreadsheet
var sheet = ss.getSheetByName("Sheet1");
// Then, the "problematic range". You get the ENTIRE range, from end to end, as such:
var wholeRange = sheet.getRange(1,1,sheet.getLastRow(),sheet.getLastColumn());
// Then, you fetch its values:
var rangeValues = wholeRange.getValues();
// At this point you have a bi-dimensional array, representing the rows and columns.
// Assuming you have 2 columns, in the first column you have the names, and in the second you have the unknown value
// You need to use the already known for loop, iterate over all the data, and store it first in an object, so that you create the file only ONCE.
var objectData = {};
for (var i=0;i<rangeValues.length;i++) {
var thisName = rangeValues[i][0];
var thisValue = rangeValues[i][1];
if (objectData.thisName == undefined) objectData.thisName = [];
objectData.thisName.push(thisValue);
}
// Now we have our values grouped by name.
// Let's create a file for each name.
for (var name in objectData) {
DriveApp.createFile(name, objectData.name.join("\n"));
}
// NOTE: if you want to create into a specific folder, you first target it, using the DriveApp:
var folders = DriveApp.getFoldersByName("the folder name goes here");
// folders variable is now an iterator, containing each folder with that name.
// we will iterate over it as follows, and select the one we want.
// the logic for it, you'll select one of your choice:
while (folders.hasNext()) {
var thisFolder = folders.next();
if (/* condition to check if we found the right folder */) {
thisFolder.createFile(name, objectData.name.join("\n"))
}
}
}

Related

I want to make a script in Google sheet to find and match strings of texts between different sheets

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.

How to delete Named Ranges with Tab names via Apps Script

I'm trying to clean a spreadsheet of a slew of Named Ranges I no longer need, and leave behind the few I'm still using. All of these Named Ranges include the Tab name, because they originate on a template Tab (named tmp), from which other Tabs are duplicated. Even after I delete all the spinoff Tabs from the sheet and leave behind only tmp, the 'tmp'! appears in the names of the Ranges, both as displayed in the Named Ranges sidebar and as they come in on getNamedRanges().
When I try to selectively delete obviated Named Ranges, no matter how I spec the name of the Ranges, I get errors saying no such Named Ranges exist. Basically, I'm feeding back the same information getNamedRanges() and getRange().getSheet().getSheetName() give me, only to have it garbled along the way.
The problem is isolated in the following test snippet, and involves rendering the single quotes around the Tab name. I have tried several approaches, including escaping the single quotes with slashes, and have added to the code the Comments of the errors I got on the line targetDoc.removeNamedRange(namedRange).
const analyzerDoc = '1pYgcX2dxzHd4cCofy0RFZTzEl36QesiakMGIqCC2QlY'
const openAnalyzerDoc = SpreadsheetApp.openById(analyzerDoc)
function testDeleteNamedRange (){
var docUrl = openAnalyzerDoc.getRangeByName('docUrl').getValue();
var targetDoc = SpreadsheetApp.openByUrl(docUrl);
// var namedRange = 'dyCl_MoodEntries' // The named range "dyCl_MoodEntries" does not exist.
// var namedRange = 'tmp!dyCl_MoodEntries' // The named range "tmp!dyCl_MoodEntries" does not exist.
// var namedRange = "'tmp'!dyCl_MoodEntries" // The named range "'tmp'!dyCl_MoodEntries" does not exist.
// var namedRange = "\'tmp\'!dyCl_MoodEntries" // The named range "'tmp'!dyCl_MoodEntries" does not exist.
targetDoc.removeNamedRange(namedRange);
}
This bug is in the way of a longer function, which is working fine but for the part isolated in this test function.
The longer function gets the names and Tabs of Ranges to delete from this sheet:
What is the right way to do this? Thank you!
This function will remove all of the named range that have their sheet name within the range name.
function deleteAllNamedRange() {
const ss = SpreadsheetApp.getActive();
ss.getNamedRanges().filter(r => ~r.getName().indexOf(r.getRange().getSheet().getName())).forEach(r => r.remove());
}
I have an answer to my own question. There is probably more than one solution, but I have chosen to sidestep the challenge I am facing, and instead of specifying the Named Ranges by name, I am going to spec them by their position in the document's Named Ranges, and simply use remove() instead of removeNamedRange(namedRange). I had gotten so caught up in the recommended method involving forEach, that I had forgotten that the outcome of getNamedRanges() is not an object, but an array.
The solution then lies in amending my process of collecting the Names and other information from the result of getNamedRanges(). Instead of using forEach, I loop over the results of getNamedRanges(), and while I get the information I desire concerning each Named Range, I also log the loop iteration and thereby get the Index Number of each Named Range.
I proceed as before, pasting this information in a Tab where I can select which Ranges to delete.
My delete function then loops over the Named Ranges directly, in reverse order, and checks the loop iteration against the Ranges I have ticked off in that analysis Tab.
I have tested this in a sample document; you may view it here.
In this demo, all functions are within the same document, so I'm using getActive() instead of openByUrl.
This document has 3 Tabs named Sheet1, Sheet2 and Sheet3. Each Tab has 3 Named Ranges named Moe, Larry and Curly. There is also a Tab NamedRanges which the following function collects Named Range into:
function getnamedRanges() {
var namedRanges = SpreadsheetApp.getActive().getNamedRanges();
var namedRangeData = [];
for (i=0; i<namedRanges.length; i++) {
var namedRange = namedRanges[i];
var nrName = namedRange.getName();
var nrRange = namedRange.getRange().getA1Notation();
namedRangeData.push([nrName,nrRange,i])
}
SpreadsheetApp.getActive().getSheetByName('NamedRanges').getRange(2,1,namedRangeData.length,3).setValues(namedRangeData)
}
Here's the Named Range Tab after running that function, and choosing 3 Named Ranges to delete:
Next, here is the function that removes the selected Named Ranges:
function deleteSelectedNamedRanges () {
var namedRangeData = SpreadsheetApp.getActive().getSheetByName('NamedRanges').getDataRange().getValues();
namedRangeData.shift(); // Remove header row data.
var rangesToDelete = namedRangeData.filter(function(nrDatum) {if (nrDatum[3]==true) return nrDatum});
// [3] equivalates to Column D, the checkboxes where I select which Named Ranges to delete.
console.log (rangesToDelete.map(value => value[0])); // [ 'Sheet3\'!Moe', 'Sheet2\'!Curly', 'Sheet1\'!Moe' ]
console.log (rangesToDelete.map(value => value[2])); // [ 0, 1, 5 ] // [2] is the index number of the Named Ranges.
/* The order here derives from how values in Tab Named Ranges happen to be sorted.
In this instance, I have not changed that order, so the Named Ranges To Delete are in ascending order.
For one thing, this is the opposite of what we want;
for another, I want to be able to sort the Named Range Tab freely before making my selections.
So, we must sort this data in DESCENDING order. */
rangesToDelete.sort(function(value1,value2){if (value1[2]<value2[2]) return 1; if (value1[2]>value2[2]) return -1; return 0});
console.log (rangesToDelete.map(value => value[0])); // [ 'Sheet1\'!Moe', 'Sheet2\'!Curly', 'Sheet3\'!Moe' ]
var rangesToDeleteIndexNumbers = rangesToDelete.map(value => value[2])
console.log (rangesToDeleteIndexNumbers); // [ 5, 1, 0 ]
var namedRanges = SpreadsheetApp.getActive().getNamedRanges();
for (i=namedRanges.length-1; i>=0; i--) {
/* We must loop in descending order because deleting Named Ranges will change the index numbers
of all Named Ranges that come after each we delete. */
if (rangesToDeleteIndexNumbers.indexOf(i) !== -1) {namedRanges[i].remove(); console.log ('Removed NR # '+i)}
// Delete Named Range if this iteration number can be found in rangesToDeleteIndexNumbers.
}
}
After running this function, you can see that the 3 Names Ranges have been removed, leaving 6 behind:

Copy data range and paste to new page, and repeat

I am trying to copy a range from sheet 'Full' and paste the values only to a new sheet, 'Dump'. While the macro below does its action once, I am regenerating the original data range (Full), so I want to copy that new set and append to the same output page, indexed down to a blank row and keeping the first pasted data. Also then to do this 100 times.
The recoded macro is below, and I need to understand the script to add in to;
repeat the copy/paste function 100 times, and also
offset the paste range by a set number of rows.
Sorry, genuine newbie at editing google sheet macros. The Excel macro I use doesn't translate over.
Appreciate any answers you have.
function xmacro() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('A1').activate();
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Full'), true);
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Dump'), true);
spreadsheet.getRange('Full!BK3:BT34').copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);```
};
Your macro is just an automatically generated app script. You can extend its functionality by building off that with some more code. First I'll explain some of the basic concepts, if you know this, then just skip down to the code.
Sheets Concepts
Here are some basic concepts that took me forever to figure out because most of the documentation assumes you are already proficient at Javascript.
A range is a 2 dimensional array that has one array for each row, and the contents of that array are the columns:
someRange = [
[row1Col1, row1Col2, row1Col3, row1Col4],
[row2Col1, row2Col2, row2Col3, row2Col4],
[row3Col1, row3Col2, row3Col3, row3Col4]
]
To access a specific value you need to reference the row array, and then the index of the column you want.
Think about it like hotel room numbers. The first part of the number is the floor,
and the second part is the specific room on that floor.
You access arrays by calling the array name, then square brackets with the index number of the element you want.
Arrays are indexed starting at 0, so to get row 1 you would use:
someRange[0] would return the inner array [row1Col1, row1Col2, row1Col3].
But that doesn't give you a specific cell values - so you would use a second set of brackets to access the column in that row:
someRange[0][1] = 'row1Col2'
Arrays also have built in information, so you can find the length of an array by using Array.length no parenthesis.
Since the rows are in the outer array, you can get the number of rows by seeing how many inner arrays there are.
someRange.length = 3 There are 3 row arrays in the someRange array.
You can do the same with columns, since the number of columns is equal to the number of elements in an array. To get the number of elements in the first row you would use:
someRange[0].length - which would be 4
And since a range has the same number of columns for each row, you can pick any row
to get the number of columns (generally, there are always exceptions)
The Code
The first function will create a custom menu item to run the code.
// create a new menu item for your custom function
function onOpen(){
SpreadsheetApp.getUi().createMenu()
.addItem('100 Copies', 'lotsOfCopies')
.addToUi();
}
function lotsOfCopies() {
var ss = SpreadsheetApp.getActive();
var copySheet = ss.getSheetByName('yourCopySheetName');
var pasteSheet = ss.getSheetByName('yourPasteSheetName');
// the range you wish to copy, change to fit your needs
var copyRange = copySheet.getRange('A1:B7');
var copyValues = copyRange.getValues();
var copyRows = copyValues.length;
var copyCols = copyValues[0].length;
// define the first row to be pasted into
var pasteRow = 1;
// define the left side column of the range to be pasted into
var pasteCol = 1
// build a loop that does the same thing 100 times,
// and each time offsets the paste range by the number of rows in the copy range
for (var i = 0; i < 100; i++) {
// for every iteration after the first,
// add the number of rows in the copy range to the variable 'row'
// example if there are 10 rows in the copy range then
// iteration 1 row = 1 Iterartion 2 row = 11, Iteration 3 row = 21
if (i > 0) {
pasteRow = +pasteRow + +copyRows
}
// build the range to paste into - it starts on pasteRow and paste col,
// and is as many rows as the copied range, and as many columns as the copied range
let pasteRange = pasteSheet.getRange(pasteRow, pasteCol, copyRows, copyCols);
// put the values from copyValues into the pasteRange
pasteRange.setValues(copyValues);
}
}
function xmacro() {
const ss = SpreadsheetApp.getActive();
const ssh = ss.getSheetByName('Full')
const dsh = ss.getSheetByName('Dump')
ssh.getRange('BK3:BT34').copyTo(dsh.getRange('A1'), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
}

Trying to limit an existing script to a specific sheet

My first issue is this, I have an items log sheet where I want to add and manage individual unique items in our inventory. I created a data validation dependent dropdown list for a main category and found out how to build a script to dynamically create a secondary category dropdown list based on the selected main category.
For Example:
If cell B2 (Main Category) is set to Carabiner (based on data validation range on another sheet) THEN cell C2 (secondary Category) will dynamically create a dropdown list relative to the Carabiner main category (i.e. locking, non-locking)
That is simple enough if you only have one row to create the dropdown lists, but I wanted to be able to pick from a secondary category list in each row dependent on which was picked in the main category cell.
I found a video of a script that did just that and got it working just fine.
Now the problem is that the script runs data validation on every other sheet. How can I limit the script to only run on a specific sheet?
Here is the script:
function onEdit() {
// this line just refers to the current file var start = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); var current = start.getActiveCell()
// var to refer to the worksheets -lists is where the data validation range will come from, and main is where we want to use that data validation range var list = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Indirect_Categ_Ranges") var main = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Items_Log");
// has the user selected a category? Refers to the column number in the Items_Log sheet where the user has picked the main category
if (current.getColumn()==2)
{
//to copy the selected sub-category -the 2,1 is the row and column on the Indirect_Categ_Ranges sheet where this script will dynamically update the main category picked to define what the indirect function will display in the next column
var choice = current.getValue()
list.getRange(2,1).setValue(choice)
//clear any validation -the 2,3,1000 looks to start clearing validation at row 2, column 3 and down for up to 1000 entries
main.getRange(2,3,5000).clearDataValidations()
// create the rule - var_point defines the offset number of rows and columns to shift to initiate the dynamic dependent dropdown list, the var_items defines where the to look for the range to build the dropdown list
var point = current.offset(0,1)
var items = list.getRange(2,2,50)
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(items,true).build();
point.setDataValidation(rule)
}
}
Also, is there a way to have the clear validations to run no matter how many rows there are?
The function will run anytime there's an edit and there's nothing you can do to stop that. You can, instead, terminate execution preemptively if it's not the sheet you care about.
The event object tells you which range was edited. You can get that range's sheet to know which sheet was edited. If the name matches, then execute the other stuff.
function onEdit(e) {
if (e.range.getSheet().getName() === 'Items_Log') {
// Data validation
}
}
It's not great practice to use .getActiveRange() or .getActiveSheet() when you want what was actually edited because there is a chance, however small, that the edited range may differ from the active range at the time of function execution.
Explanation:
You need to take advantage of the event object.
That object contains relevant information to the edits you make.
For example:
e.source is equivalent to SpreadsheetApp.getActive()
e.range is equivalent to .getActiveCell().
To run the code only for a particular sheet, in this case Items_Log, add a condition to check if the name of the active sheet matches that name:
if (current.getColumn()==2 && start.getName()=="Items_Log")
where start is the active sheet:
var start = e.source.getActiveSheet();
Solution:
function onEdit(e) {
var start = e.source.getActiveSheet();
var current = e.range;
var list = e.source.getSheetByName("Indirect_Categ_Ranges")
var main = e.source.getSheetByName("Items_Log");
if (current.getColumn()==2 && start.getName()=="Items_Log")
{
var choice = current.getValue()
list.getRange(2,1).setValue(choice)
main.getRange(2,3,5000).clearDataValidations()
var point = current.offset(0,1)
var items = list.getRange(2,2,50)
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(items,true).build();
point.setDataValidation(rule)
}
}
Thanks for the help. after looking at your suggestions and trying a couple things, I found that simply adding the code:
&& start.getName()=="Items_Log")
To the end of the line:
if (current....
Worked and solved the issue.

Get all the ID of the files

I'm working on a folder per grade level with the same content. I used the IMPORT RANGE function to connect the spreadsheets. So I need to update the IDs every time I create another grade level. May I ask if it is possible for me to get all the IDs of all the files that I'm working on without editing or typing them one by one? Maybe a script that can make it easier?
Here's the picture of the list of IDs.
If it is possible, the flow I’m thinking of is to get the filename of the files and then get the id of it.
Explanation:
The following script will get all the file names in the range B3:B. Then forEach file name we use getFilesByName to get the files corresponding to that name. Make sure you don't have files with the same name, otherwise this process will return multiple IDs for the same name and therefore the logic won't work.
Finally we use getId to get the ID of each file and store it into an array. This array will be then pasted to column C starting from cell C3 as you indicate in the screenshot.
Solution:
function myFunction() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet1'); // change that to the name of your sheet
const filenames = sh.getRange('B3:B').getValues().flat().filter(r=>r!='');
const IDs = [];
const folderId = "put your folder id here";
const Folder=DriveApp.getFolderById(folderId);
filenames.forEach(fn=>{
let Files = Folder.getFilesByName(fn);
while(Files.hasNext()){
let file = Files.next();
IDs.push([file.getId()]);
}
});
sh.getRange(3,3,IDs.length,1).setValues(IDs);
}