Google Spreadsheet Query using Dynamically defined Sheets - google-apps-script

i was strugling with something unusual or not...
I am doing an google spreadsheet, that will have a lot of sheets, and their names are dates as MM/YYYY, and i need to get all the data from a range A1:B100 for example, and do QUERY stuff on an master sheet with that data, the problem is, i have done a function with javascript to get the Range of that sheets, but i can´t use them on =QUERY() function, tryed a lot, with different aproaches from internet, but nothing successfull yet..
My sample spreadsheet:
https://docs.google.com/spreadsheets/d/1W5W7y16yvOUoqNZCZ59ol8D2nbelJHNqtgaxrtb1jC8/edit?usp=sharing
Also the app script i have done to manage the sheets data dynamically:
let sheetsData = () => SpreadsheetApp.getActiveSpreadsheet().getSheets().filter(sheet => sheet.getName().match(/^[\d]/)).map(sheet => sheet.getRange("A1:B20"));
Logger.log( sheetsData() );
My sample usage on the spreadsheet is =QUERY(sheetsData();"SELECT *"), i can´t even list the data... lul
Any help will be most welcome, tnx.

Instead of returning the Range objects, you need to return the values contained within the ranges. You also have to make sure that the data you return is structured as a two-dimensional array.
function GET_DATA() {
const dataSheets = SpreadsheetApp.getActiveSpreadsheet()
.getSheets()
.filter(s => s.getName().match(/^[\d]/));
const ranges = dataSheets
.map(s => s.getRange("A1:B20"));
return ranges
.reduce((result, range) => result.concat(range.getValues()), []);
}
The above implementation works with a formula such as =QUERY(GET_DATA(), "SELECT *").
However, there's a fundamental problem with this approach. Since the range location is hardcoded on the Apps Script side and not specified as part of the formula, recalculations are not triggered as data in the sheets change.

Related

I need help creating Install trigger to replace onEdit()

I have created a spreadsheet that receives form input, fiddles about with the data and then I want to view one row of data by selecting a record based on an id field.
I read and watch a lot of vids and found this brilliant video that showed me how to write a script for it.
I made a simplified spreadsheet SimplifiedDB2023
and a script
//DummyDatabse2023
//const ss = SpreadsheetApp.openById("11Fw0OmFpvkkfh11c95gBd7qEWifGJO6eUv38UwKRxNU")
//SimplifiedDB2023
//const ss = SpreadsheetApp.openById("1c-y_XUuK0L86qUUX471-GdBT-CN5GNTk4EGK7G57_94")
const ss = SpreadsheetApp.getActiveSpreadsheet()
const updateWS = ss.getSheetByName ("Form")
const mergeddataWS = ss.getSheetByName ("mergeddata")
const fieldRange = ["P5","G7","C24","C25","D7","C27","J7","I9","J9","H9","G9","F9","D9","D11","G11","G12","C38","E14","C40","C41","C42","C43","H5","R7","R12","P10","C48","C49","C50","C51","C52","C53","C54","C55","C56","P6","P12","P14","P16","P8","R8","D5"]
const searchCell = updateWS.getRange("E2")
function search() {
const searchValue = searchCell.getValue()
const data = mergeddataWS.getRange("A2:AQ").getValues()
const recordsFound = data.filter(r => r[42] == searchValue)
if(recordsFound.length === 0) return
fieldRange.forEach((f,i) => updateWS.getRange(f).setValue(recordsFound[0][i]))
}
which worlked beautifully. When I edit cell E2. (Basically choosing from a list), using the onEdit function,
SimplifiedScript2023
it selects all the information from that row using a filter in the search() function..
When I followed the exact same procedure for my more complicated and involved spreadsheet,
DUMMYDB2023
I ran into issue. The problem seems to be that the second script I wrote
( which I basically copied and pasted ) doesn't seem to be "bound" to the sheet, so I cannot use : const ss = SpreadsheetApp.getActiveSpreadsheet().
as I get an error :
TypeError: Cannot read properties of null (reading 'getSheetByName')
I looked into this and then when I changed
const ss = SpreadsheetApp.openById("11Fw0OmFpvkkfh11c95gBd7qEWifGJO6eUv38UwKRxNU")
It worked, but I am not able to use the onEdit function, which is vital, because I want any edits to cell E2, to trigger the function (search).
So my questions are - Why does the second version (- DummyDB2023) not work - I suspect this is because this more developed sheet has links to external sources like the forms, but even so, I am the owner of the spreadsheet and I followed the exact same procedure to create the script so it 'should' be bound, but apparently is not.
The second question is, if I must use the openById, I need to create an installable trigger. I thought this would be quite simple, but I cannot find any comprehensible resource that shows how to create an installable trigger for when a single cell is edited.
Can anyone help me write a script for a trigger that runs my search function when cell E2, on sheet Form! is edited?
As always, thanks for your time and help.
I created a stripped down version of my spreadsheet and the procedure worked perfectly.
When I applied it to my full spreadsheet, it wouldn'T run.

Get Sheet By Name with an OR statement

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');

Google Sheets Print All IDs in one click

I'm working on a student information template and I'm wondering if it's possible to print all of the data for each student in one go? I used data validation on my spreadsheet to modify the student ID so that their data will be easily view and print. Because I have to print it one by one for each pupil, this is a time-consuming process so I come up with this kind of flow to save time. Is it possible?
Please see the sample spreadsheet.
Upon printing, there should be a different page per student.
I believe your goal is as follows.
You want to reduce the cost of printing the data from the Spreadsheet.
I thought that when the Spreadsheet is printed out using Google Apps Script, the Google Cloud Print can achieve this. But I thought that in this case, the settings might be a bit complicated. So, in this case, I would like to propose a workaround. How about the following flow?
Retrieve the values from the source sheet and create the output values as the array.
Create a new Spreadsheet and put each value on each page.
Print out the Spreadsheet.
By this flow, you can print out all pages by one manual process.
The sample script is as follows.
Sample script:
Please copy and paste the following script to the script editor of Google Spreadsheet. And run myFunction.
function myFunction() {
const rowHeader = ["STUD ID", "LAST NAME", "FIRST NAME", "MIDDLE NAME", "NAME EXTENSION"];
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("DATA");
const [header, ...values] = sheet.getDataRange().getValues();
const ar = values.map(r => header.reduce((o, h, i) => Object.assign(o, { [h.toUpperCase()]: r[i] }), {}));
const newValues = ar.map(e => [rowHeader, ...[rowHeader.map(f => e[f])]]).map(e => e[0].map((_, c) => e.map(r => r[c])));
const ss = SpreadsheetApp.create("tempSpreadsheet");
newValues.forEach((v, i) => (i == 0 ? ss.getSheets()[0] : ss.insertSheet()).getRange(1, 1, v.length, v[0].length).setValues(v));
}
When you run this script, a new Spreadsheet of tempSpreadsheet is created to the root folder. When you open it, you can see the expected values for each worksheet. By this, you can print out them.
Note:
This sample script is prepared from your sample Spreadsheet. So when your sample Spreadsheet is different from your actual situation, this sample script might not be able to be used. Please be careful about this.
References:
reduce()
map()
create(name)

App Script Conditional Formatting to apply on sheet by name

I have been trying to make a Google App Script code which highlight the cell if it has specific text like "L".
I have made a below code but its not working and when i run this no error appears. i do not know what is the problem.
Can you have a look at it, please that why its not working.
function formatting() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Dec');
var range = sheet.getRange("C:AG");
if (range == 'L') {
ss.range.setBackgroundColor('#ea9999');
}
}
Issues with the code:
Three things to mention:
range is a range object, not a string. In the if condition you are comparing an object of type range with an object of type string. You need to use getValue to get the values of the range object and then compare that with the string L.
This code will take a lot of time to complete because you have a large range of cells you want to check but also you are iteratively using GAS API methods. As explained in Best Practices it is way more efficient to use batch operations like getValues,
getBackgrounds and setBackgrounds.
Another improvement you can make is to use getLastRow to restrict the row limit of your range since you are looking for non-empty values. There is no reason for checking empty cells after the last row with content.
Google Apps Script Solution:
function formatting() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Dec');
const range = sheet.getRange("C1:AG"+sheet.getLastRow());
const values = range.getValues();
const bcolors = range.getBackgrounds();
const new_bcolors = values.map((r,i)=>r.map((c,j)=>c=='L'?'#ea9999':bcolors[i][j]))
range.setBackgrounds(new_bcolors)
}
Google Sheets Solution:
Another idea would be to just create a conditional formatting in Google Sheets:
and specify a custom color with your hex code:
JavaScript References:
map
ternary operator

Sheets Filter Script across multiple tabs displaying blank rows on master sheet, and also causing other scripts to stop functioning

I have a multifaceted question.
I'm attempting to have a script which pulls the tab names, and uses that info to pull specific cells from each one (tabs for the doc change frequently) in order to create a Master Sheet. The Master Sheet is meant to display all open action items, and filter out any closed items / blank rows. The script I have so far works, but it pulls all empty rows from each tab - which I don't want.
Here's 1 of the 2 current scripts I have for the master sheet:
function onEdit(e) {
//set variable
const masterSheet = "Open Action Items";
const mastersheetFormulaCell = "E2";
const ignoreSheets = ["Template", "Blank"];
const dataRange = "I2:K";
const checkRange = "J2:J"
//end set variables
const ss = SpreadsheetApp.getActiveSpreadsheet();
ignoreSheets.push(masterSheet);
const allSheets = ss.getSheets();
const filteredListOfSheets = allSheets.filter(s => ignoreSheets.indexOf(s.getSheetName()) == -1);
let formulaArray = filteredListOfSheets.map(s => `iferror(FILTER('${s.getSheetName()}'!${dataRange}, NOT(ISBLANK('${s.getSheetName()}'!${checkRange}))),{"","",""})`);
let formulaText = "={" + formulaArray.join(";") + "}";
ss.getSheetByName(masterSheet).getRange(mastersheetFormulaCell).setFormula(formulaText);
}
The other part of this is another script that has been running ok when it was the only script running, but has since stopped working when the other script were introduced. This script added a checkbox to column C based on criteria in column B.
Here's that script:
function onEdit(e) {
if (e.range.getColumn() == 2) {
var sheet = e.source.getActiveSheet();
if (e.value === "Tech Note" ||
e.value === "Intake Process")
sheet.getRange(e.range.getRow(),3).insertCheckboxes();
else
sheet.getRange(e.range.getRow(),3).removeCheckboxes();
}
}
Here's a sample sheet
The "Open Action Items" tab is the master sheet the script is meant to update. It should list all the open items from other tabs (explained below)
The "Copy of E3-O Case Notes" is a tab which is the basis of what every tab will eventually look like. Columns F-K of this tab pull open items from A-E. There may likely be a more efficient way to sort this whole sheet...
Any help appreciated, thank you!
I'll address the second question first, as it's a more fundamental problem.
The other part of this is another script that has been running ok when it was the only script running, but has since stopped working when the other script were introduced.
In the script project attached to your sample, you have 3 files which each define an onEdit() function. This is problematic because each time you define onEdit() you're redefining the same identifier. The project only has a single global scope, so there can only be 1 onEdit() function defined, regardless of how many files your project contains.
Essentially, this is equivalent to what you've defined in your project:
function onEdit(e) {
console.log("onEdit #1");
}
function onEdit(e) {
console.log("onEdit #2");
}
function onEdit(e) {
console.log("onEdit #3");
}
onEdit();
Running the above snippet will only execute the last definition of onEdit().
To accomplish what you're trying to do, you can instead define unique functions for all the actions you want to perform and then, in a single onEdit() definition, you can call those functions. Something like:
function editAction1(e) {
console.log("edit action #1");
}
function editAction2(e) {
console.log("edit action #2");
}
function editAction3(e) {
console.log("edit action #3");
}
function onEdit(e) {
editAction1(e);
editAction2(e);
editAction3(e);
}
onEdit();
When defining an onEdit() trigger, you really want to optimize it so that it can complete its execution as quickly as possible. From the Apps Script best practices, you want to pay particular attention to "Minimize calls to other services" and "Use batch operations".
A few specific tips for you:
Avoid repeated calls to the same Apps Script API (e.g. Sheet.getName()). Instead, run it once and store the value in local variable.
As much as possible, avoid making Apps Script API calls within loops and in callback functions passed to methods such as Array.prototype.filter() and Array.prototype.map().
When you do need to loop through data, especially when Apps Script API calls are involved, minimize the number of times you iterate through the data.
With onEdit() triggers, try to structure the logic so that you identify cases where you can exit early (similar to how you perform the column check before going ahead with manipulating checkboxes). I doubt you actually need to iterate through all of the sheets and update the "Open Action Items" formula for every single edit. If I'm interpreting the formula properly, it's something that should only be done when sheets are added or removed.
Finally, to address the blank rows in your formula output, instead of using SORT() to group the blank rows you can use QUERY() to actually filter them out.
Something like:
=QUERY({ <...array contents...> }, "select * where Col1 is not null")
Note that when using QUERY() you need to be careful that the input data is consistent in regards to type. From the documentation (emphasis mine):
In case of mixed data types in a single column, the majority data type
determines the data type of the column for query purposes. Minority
data types are considered null values.
In your sample sheet, a lot of the example data varies and doesn't match what you'd actually expect to see (e.g. "dghdgh" as a value in a column meant for dates). This is important given the warning above... when you have mixed data types for a given column (i.e. numbers and strings) whichever type is least prevalent will silently be considered null.
After taking a closer, end-to-end look at your sample, I noticed you're performing a very convoluted series of transformations (e.g. in the data sheets there's the hidden "D" column, the QUERY() columns to the right of the actual data, etc.). This all culminates in a large set of parallel QUERY() calls that you're generating via your onEdit() implementation.
This can all be made so much simpler. Here's a pass at simplifying the Apps Script code, which is dependent on also cleaning up the spreadsheet that it's attached to.
function onEdit(e) {
/*
Both onEdit actions are specific to a subset of the sheets. This
regular expression is passed to both functions to facilitate only
dealing with the desired sheets.
*/
const validSheetPattern = /^E[0-9]+/;
updateCheckboxes(e, validSheetPattern);
updateActionItems(e, validSheetPattern);
}
function updateCheckboxes(e, validSheetPattern) {
const sheet = e.range.getSheet();
// Return immediately if the checkbox manipulation is unnecessary.
if (!validSheetPattern.exec(sheet.getName())) return;
if (e.range.getColumn() != 2) return;
const needsCheckbox = ["Tech Note", "Intake Process"];
const checkboxCell = sheet.getRange(e.range.getRow(), 3);
if (needsCheckbox.includes(e.value)) {
checkboxCell.insertCheckboxes();
} else {
checkboxCell.removeCheckboxes();
}
}
function updateActionItems(e, validSheetPattern) {
const masterSheetName = "Open Action Items";
const dataLocation = "A3:E";
/*
Track the data you need for generating formauls in an array
of objects. Adding new formulas should be as simple as adding
another object here, as opposed to duplicating the logic
below with a growing set of manually indexed variable names
(e.g. cell1/cell2/cell3, range1/range2/range3, etc.).
*/
const formulas = [
{
location: "A3",
code: "Tech Note",
},
{
location: "E3",
code: "Intake Process",
},
];
const masterSheet = e.source.getSheetByName(masterSheetName);
const sheets = e.source.getSheets();
/*
Instead of building an array of QUERY() calls, build an array of data ranges that
can be used in a single QUERY() call.
*/
let dataRangeParts = [];
for (const sheet of sheets) {
// Only call getSheetName() once, instead of multiple times throughout the loop.
const name = sheet.getSheetName();
// Skip this iteration of the loop if we're not dealing with a data sheet.
if (!validSheetPattern.exec(name)) continue;
dataRangeParts.push(`'${name}'!${dataLocation}`);
}
const dataRange = dataRangeParts.join(";");
for (const formula of formulas) {
/*
And instead of doing a bunch of intermediate transformations within the sheet,
just query the data directly in this generated query.
*/
const query = `SELECT Col5,Col1,Col4 WHERE Col2='${formula.code}' AND Col3=FALSE`;
const formulaText = `IFERROR(QUERY({${dataRange}},"${query}"),{"","",""})`;
formula.cell = masterSheet.getRange(formula.location);
formula.cell.setFormula(formulaText);
}
}
Here's a modified sample spreadsheet that you can reference.
The one concession I made is that the data sheets still have a "Site Code" column, which is automatically populated via a formula. Having all the data in the range(s) you feed into QUERY() makes the overall formulas for the "Open Action Items" sheet much simpler.