Passing an array into a custom function wrapped in an ArrayFormula? - google-apps-script

I have a spreadsheet that contain columns that use merged cells for formatting reasons. I am trying to create columns that mirror this first set of columns but used the merged cell value on all of its affected rows. I can do this thanks to a custom function that I found online. What I can't do is then contain this within an arrayformula and I'm not sure why.
Here is a small version of the spreadsheet:
https://docs.google.com/spreadsheets/d/1mp8PpgO4sI60bbx__1L4a17qL1VGIB9QVB910vTyhg0/edit?usp=sharing
The custom function is:
/**
* Takes into account merged cells and returns the value of the merged cell
* for all the cells within the merged range, rother than just the top left
* cell of the merged range.
*
* Copied from https://webapps.stackexchange.com/questions/110277/how-do-i-reference-the-values-of-merged-cells-in-formulas
*
* Used by Patrick Duncan. - 7 May 2018
*/
function cellVal(cellAddress) {
var cell = SpreadsheetApp.getActiveSheet().getRange(cellAddress);
return (cell.isPartOfMerge() ? cell.getMergedRanges()[0].getCell(1, 1) : cell).getValue();
}
The formula without the Arrayformula is:
=cellVal2(index(address(row(),5,4)))
And what I was trying was:
=arrayformula(cellVal2(index(address(row(E3:E),5,4))))
Any idea what I'm doing wrong here?
Cheers,
Patrick

How about this modification? I think that there are several solutions for your situation. So please think of this as one of them.
Modification points :
When index(address(row(E3:E30),5,4)) is given to cellAddress, cellAddress is [["E3"], ["E4"], ["E5"],,,]. This is a 2 dimensional array.
Flatten this 2 dimensional array for getRangeList().
Convert the flattened array to the range array using getRangeList().
Retrieve each value using the converted range and return them.
Modified script :
function cellVal3(cellAddress) { // Modified the function name to "cellVal3"
cellAddress = Array.prototype.concat.apply([], cellAddress);
var cells = SpreadsheetApp.getActiveSheet().getRangeList(cellAddress).getRanges();
return cells.map(function(cell){return [(cell.isPartOfMerge() ? cell.getMergedRanges()[0].getCell(1, 1) : cell).getValue()]});
}
Usage :
When you use this custom function, please use as follows.
=ARRAYFORMULA(cellVal3(index(address(row(E3:E30),5,4))))
or
=cellVal3(index(address(row(E3:E30),5,4)))
References :
Array.prototype.concat()
getRangeList()
If I misunderstand your question, I'm sorry.

Related

How To Add The Ordinal Count To Values Set In Horizontal Non-Contiguous Ranges In Google Sheets With A Formula Or A Script? EDIT ADDRESS FUNCTION

Problem:
I'm trying to add each ordinal reference to a set of repeating values in each cells just above each value.
The values are organized in horizontal and non-contiguous order.
The illustration example I show below is simple for testing purposes, but the end use should be for hundreds of values/ranges, so it would be optimal to use a script or a simplified version of the formula I found.
Illustration Example:
Other Related Question and Solution:
I found that question and answers that address the same question but for vertical and contiguous values using the following formula as solution:
=COUNTIF(A$1:A1,A1)
=COUNTIF(A$1:A1,A1)&MID("thstndrdth",MIN(9,2*RIGHT(COUNTIF(A$1:A1,A1))*(MOD(COUNTIF(A$1:A1,A1)-11,100)>2)+1),2)
Calculate ordinal number of replicates
My Formula So Far:
=TRANSPOSE(INDIRECT($P$21&(SUM(Q21))&":"&$P$21&(SUM(Q21,I22)-1)))
=TRANSPOSE(INDIRECT($P$21&(SUM(Q21,I22))&":"&$P$21&(SUM(Q21,I22,I26)-1)))
=TRANSPOSE(INDIRECT($P$21&(SUM(Q21,I22,I26))&":"&$P$21&(SUM(Q21,I22,I26,I30)-1)))
I use the above formula and need to copy-paste it in the cell immediately above the 1st cell of each horizontal range.
I need to reference each cell in the SUM Functions part because the spreadsheet will act as a template, with new data sets that will be different each time.
Therefore the cells need to return output in some dynamic way (can't hardcode them).
The formula problem is it requires an ever growing number of cells reference as we get to new ranges. It becomes difficult for hundreds of horizontal ranges, because of the growing inline cells to add to the SUM Functions.
It is also prone to errors. And possibly it can break if rows or columns are added afterwards.
Trials:
I originally didn't think of using the INDIRECT Function (I never needed before). But I don't know any other Google Sheets function able to achieve the end results in a simpler way.
Questions:
What way to avoid the SUM Function method for the same result would you suggest, for a formula solution?
For a formula, what simpler function-s than the INDIRECT and/or SUM would be more efficient?
I also thought of using a script for doing that, but I can't put the whole idea into a manageable script concept process. What would you suggest if a script would be more appropriate?
Many thanks for your help!
The Sample Sheet:
Sample Sheet
EDIT:
I just found about the ADDRESS Function from this answer by Player0 (to help greatly simplify the INDIRECT function row and column references):
Google Sheets: INDIRECT() with a Range
References:
Excel INDIRECT Function
Google Sheets ADDRESS Function
I was able to create a script to show the ordinal number of the replicates but is only respective to one range. EDIT: I have modified it to also accept multiple row ranges. See updated answer below:
Script:
function showOrdinal(range) {
range = "A4:E12";
var values = SpreadsheetApp.getActiveSheet().getRange(range).getValues();
var output = [];
var order, subTotal;
values.forEach((x, i) => {
if(x.flat().filter(String).length) {
subTotal = values.slice(0, i + 1).flat().filter(String);
order = x.filter(String);
if (order[0] != '-') {
var row = order.map(x => {
return getNumberWithOrdinal(subTotal.filter(e => e == x).length);
})
row = [...row, ...Array(x.length - row.length)];
output.push(row);
if(output.length > 2)
output.splice(output.length - 2, 1);
}
order = [...order, ...Array(x.length - order.length)];
output.push(order)
}
else
output.push(x);
});
// console.log(output);
SpreadsheetApp.getActiveSheet().getRange(3, 21, output.length, output[0].length).setValues(output);
// return output;
}
// get ordinal number
// https://stackoverflow.com/a/31615643/14606045
function getNumberWithOrdinal(n) {
var s = ["th", "st", "nd", "rd"],
v = n % 100;
return n + (s[(v - 20) % 10] || s[v] || s[0]);
}
Output:
Note:
The range that is to be passed assumes that the first row should be the first range, not a blank one. And also the last row of the range should contain the last row of the data.
Any unrelated data should be starting with - on the first column so it can be allowed without processing it.

Google Sheets Apps Script, return random item from named range

I'm trying to create a custom function that I can give a name range as input and have it output a random item from the name range. I have multiple named ranges so it would be convenient to have one function that I could use for all of them. This is what I'm trying to replace =INDEX(named_range,RANDBETWEEN(1,COUNTA(named_range)),1)
This is what I've tried but it doesn't work:
function tfunction(n) {
var randomstuffs = SpreadsheetApp.getActiveSpreadsheet().getRangeByName(n);
var randomstuff = randomstuffs[Math.floor(Math.random()*randomstuffs.length)];
Logger.log(randomstuff);
}
Thanks in advance
You can try this edited script:
function tfunction(n) {
//randomstuffs will only get all cell data that are not empty from a named range
var randomstuffs = [].concat.apply([], SpreadsheetApp.getActiveSpreadsheet().getRangeByName(n).getValues()).filter(String);
var randomstuff = randomstuffs[Math.floor(Math.random()*randomstuffs.length)];
return randomstuff;
}
Sample Result
Created a sample named range TestRange on Column A with 21 cells of data then tried the custom function =tfunction("TestRange") which returned a random cell value.
.getRangeByName() method returns a reference to a range, not the values in the range. You need to add .getValues() to it:
var randomstuffs = SpreadsheetApp.getActiveSpreadsheet().getRangeByName(n).getValues();
Also keep in mind that .getValues() method returns a 2D array of values, indexed by row, then by column. So your var randomstuff declaration will need to be change to account for that, depending on how many rows and columns your range has.

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

Fetch text string from specific column and select range of that string

I have shared below link of my sheet.
I tried every possible script to accomplish below task in the end in frustration i wipe out my whole script.
I would like to match text from column A and return or getValue of corresponding B column.
So I can use that getValue from its corresponding B column for further arithmetic operations.
Thank you.
sheet link - https://docs.google.com/spreadsheets/d/1SwYYacz9A9s6ZXrL44KtRN7gqnwXkhu4cDILdUA1iJQ/edit?usp=sharing
Basic steps:
Retrieve your data range
Form an 1D array out of your column A entries, e.g. with map()
Check either the search string is contained in the array - and if yes retrieve its position - e.g. with indexOf()
Retrieve the value with the respective row index in column B
Sample:
function myFunction() {
var matchText = "C";
var values = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getDataRange().getValues();
var columnA =values.map(function(e){return e[0]});
var row = columnA.indexOf(matchText);
if (row >= 0){
var Bvalue = values[row][1];
Logger.log(Bvalue);
}
}
I encourage you to take some time to study Apps Script, so you cannot only understand this code and adapt to your needs, but also write your own scripts.

Google Apps Script to VMerge tables WITH FORMATTING

Does anyone know if there is a Google apps script out there that does what VMerge does but keeps the formatting of the tables being merged together? (in Google Spreadsheets)
VMerge is a script that can be used as a custom formula but a script that I can trigger myself will do just fine too.
Any help would be much appreciated.
VMerge expects arrays-of-values as parameters, and therefore does not know what cells were referenced creating those arrays. When used as a Custom Formula, the sheet parser resolves all range parameters into their values before passing them to VMerge. Additionally, the parameters may be hard-coded or be the result of Queries or other functions that return ranges. Because of this alone, it's not feasible to modify VMerge to copy cell formats to the new merged table.
Complicating things further, Custom Functions cannot modify cells outside of the one they are attached to, they can only return values or arrays of values. From comment in Issue 37:
2) Scripts used as cell functions are not allowed to do complex things
like connect to other APIs or set the values of other cells. Scripts
used as cell functions are only allowed to return a value.
So you're going to have to settle for a function you call from scripts. The following function will join multiple ranges into a new table at a given anchor point. Because I started out trying to make this a custom function callable from a sheet, the parameters are string expressions of ranges, in a1Notation. (It could easily be refactored to deal directly with Range objects.)
The "Anchor" for the new range is expected to be a cell. One or more ranges of any size may be joined - each will be positioned directly below the previous.
Examples:
VJoin("D1","A1:B"); - All of columns A & B duplicated in columns D & E
VJoin("Sheet2!A1","Sheet1!C9:E10","Sheet1!A14:B15"); - Two different ranges in Sheet 1 joined and copied to Sheet 2.
Here's the code:
/*
* Vertically join the ranges from multiple sources into a new table
* starting at the given anchor point. Values and formatting are copied.
*
* #param {a1Notation} anchorA1 Anchor for joined table.
* #param {a1Notation} sources One or more source ranges.
*/
function VJoin(anchorA1,sources) {
var sheet = SpreadsheetApp.getActiveSheet();
var anchor = sheet.getRange(anchorA1);
var anchorSheet = anchor.getSheet(); // in case anchorA1 is not on the "active sheet"
var nextAnchor = anchor;
for (var i in arguments) {
// Arguments are expected to be Strings, containing a1Notation.
if (i == 0) continue; // First argument was anchorA1, skip it.
if (arguments[i].constructor == String) {
var source = sheet.getRange(arguments[i]);
var destination = anchorSheet.getRange(nextAnchor.getRow(), nextAnchor.getColumn(),
source.getNumRows(), source.getNumColumns() );
// Copy all values & formatting to new location.
source.copyTo(destination);
// Prepare for next range by moving our anchor
nextAnchor = sheet.getRange(nextAnchor.getRow() + source.getNumRows(),
nextAnchor.getColumn());
}
else {
throw new Error ("Expected String containing a1Notation.")
}
}
}
If you need a separate script to bring over the formatting...
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName('Sheet1');
s.getRange('A1').copyFormatToRange(sheet, column, columnEnd, row, rowEnd);
}
I find the below built in functions to work well pulling information from different Google Sheet files. I have defined named ranges to define what columns to pull into the Master, and also know I am having an issue with Feb.
=sort(arrayformula({
importrange("1sTS3AUfoXqXYrMYJrro9pGEKwqVL_k854yhniNOHNWc","JCJan");
importrange("1ETSD4J-8AI-7pVK0hXJKaWtG3RlHKpnco88Yj8sqNN8","JCFeb")}),1,True)