How to pass a cell reference to an Apps Script custom function? - google-apps-script

Assuming that:
A1 = 3
B1 = customFunc(A1) // will be 3
In my custom function:
function customFunc(v) {
return v;
}
v will be 3. But I want access the cell object A1.
The following is transcribed from the comment below.
Input:
+---+---+
| | A |
+---+---+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
+---+---+
I want to copy A1:A4 to B1:C2 using a custom function.
Desired result:
+---+---+---+---+
| | A | B | C |
+---+---+---+---+
| 1 | 1 | 1 | 2 |
| 2 | 2 | 3 | 4 |
| 3 | 3 | | |
| 4 | 4 | | |
+---+---+---+---+

To achieve the desired result of splitting an input list into multiple rows, you can try the following approach.
function customFunc(value) {
if (!Array.isArray(value)) {
return value;
}
// Filter input that is more than a single column or single row.
if (value.length > 1 && value[0].length > 1) {
throw "Must provide a single value, column or row as input";
}
var result;
if (value.length == 1) {
// Extract single row from 2D array.
result = value[0];
} else {
// Extract single column from 2D array.
result = value.map(function (x) {
return x[0];
});
}
// Return the extracted list split in half between two rows.
return [
result.slice(0, Math.round(result.length/2)),
result.slice(Math.round(result.length/2))
];
}
Note that it doesn't require working with cell references. It purely deals with manipulating the input 2D array and returning a transformed 2D array.
Using the function produces the following results:
A1:A4 is hardcoded, B1 contains =customFunc(A1:A4)
+---+---+---+---+
| | A | B | C |
+---+---+---+---+
| 1 | a | a | b |
| 2 | b | c | d |
| 3 | c | | |
| 4 | d | | |
+---+---+---+---+
A1:D4 is hardcoded, A2 contains =customFunc(A1:D4)
+---+---+---+---+---+
| | A | B | C | D |
+---+---+---+---+---+
| 1 | a | b | c | d |
| 2 | a | b | | |
| 3 | c | d | | |
+---+---+---+---+---+
A1:B2 is hardcoded, A3 contains =customFunc(A1:B2), the error message is "Must provide a single value, column or row as input"
+---+---+---+---------+
| | A | B | C |
+---+---+---+---------+
| 1 | a | c | #ERROR! |
| 2 | b | d | |
+---+---+---+---------+
This approach can be built upon to perform more complicated transformations by processing more arguments (i.e. number of rows to split into, number of items per row, split into rows instead of columns, etc.) or perhaps analyzing the values themselves.
A quick example of performing arbitrary transformations by creating a function that takes a function as an argument.
This approach has the following limitations though:
you can't specify a function in a cell formula, so you'd need to create wrapper functions to call from cell formulas
this performs a uniform transformation across all of the cell values
The function:
/**
* #param {Object|Object[][]} value The cell value(s).
* #param {function=} opt_transform An optional function to used to transform the values.
* #returns {Object|Object[][]} The transformed values.
*/
function customFunc(value, opt_transform) {
transform = opt_transform || function(x) { return x; };
if (!Array.isArray(value)) {
return transform(value);
}
// Filter input that is more than a single column or single row.
if (value.length > 1 && value[0].length > 1) {
throw "Must provide a single value, column or row as input";
}
var result;
if (value.length == 1) {
// Extract single row from 2D array.
result = value[0].map(transform);
} else {
// Extract single column from 2D array.
result = value.map(function (x) {
return transform(x[0]);
});
}
// Return the extracted list split in half between two rows.
return [
result.slice(0, Math.round(result.length/2)),
result.slice(Math.round(result.length/2))
];
}
And a quick test:
function test_customFunc() {
// Single cell.
Logger.log(customFunc(2, function(x) { return x * 2; }));
// Row of values.
Logger.log(customFunc([[1, 2, 3 ,4]], function(x) { return x * 2; }));
// Column of values.
Logger.log(customFunc([[1], [2], [3], [4]], function(x) { return x * 2; }));
}
Which logs the following output:
[18-06-25 10:46:50:160 PDT] 4.0
[18-06-25 10:46:50:161 PDT] [[2.0, 4.0], [6.0, 8.0]]
[18-06-25 10:46:50:161 PDT] [[2.0, 4.0], [6.0, 8.0]]

Related

Counting number of occurrences in column

Example (column A is input, columns B and C are to be auto-generated):
| A | B | C |
+-------+-------+-------+
| Name | Name | Count |
+-------+-------+-------+
| Joe | Joe | 2 |
| Lisa | Lisa | 3 |
| Jenny | Jenny | 2 |
| Lisa | | |
| Lisa | | |
| Joe | | |
| Jenny | | |
I know I can do this with the function below. However, I would like to do it with app scripts. I have tried nesting 2 for loops called (i & j). Where it started with i and j counts until it doesn't match i. Then j equals i so i just jumps to the new start point as to not double count and it kind of worked.
I kept having a problem where it would output a high number on the last 2 iterations or so... I don't think I saved the script after I could not get it to work. any thoughts or help would be appreciated.
The formula I would like to make a script:
=ArrayFormula(QUERY(A1:A16&{"",""},"select Col1, count(Col2) where Col1 != '' group by Col1 label count(Col2) 'Count'",1))
Try this:
It will read column A with header and generate B and C with headers
function pv() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getActiveSheet();
const rg=sh.getRange(2,1,sh.getLastRow()-1,sh.getLastColumn());
const vs=rg.getValues();
let Obj={pA:[]};
vs.forEach(function(r,i){
if(!Obj.hasOwnProperty(r[0])) {
Obj[r[0]]=1;
Obj.pA.push(r[0]);
}else{
Obj[r[0]]+=1;
}
});
let oA=[["Name","Count"]];
Obj.pA.forEach(function(p) {
oA.push([p,Obj[p]]);
});
sh.getRange(1,2,oA.length,2).setValues(oA);
}
This is a simple pivot table

How to use a custom function with an ArrayFormula

I want to write a function that can be used inside an ArrayFormula. My table is like this:
| A | B | C |
1| a | | |
2| b | | |
3| c | | |
First I wrote a simple function to return the input (so I know it works inside the ArrayFormula):
function retAddress(cell){
return cell;
}
On B1 I wrote =ArrayFormula(retAddress(address(row(B:B),column(A:A),4))) and apparently it worked as expected, it returned each address, like this:
| A | B | C |
1| a | A1| |
2| b | A2| |
3| c | A3| |
Now, on column C, I wanted to return the values of column A, so I wrote a function like this:
function retValue(cell){
var cellRang = SpreadsheetApp.getActive().getRange(cell);
return cellRang.getValue();
}
And on C1 I wrote =ArrayFormula(retValue(address(row(B:B),column(A:A),4))) but it gives me error Exception: Range not found (line 2)., which is the line with getRange(cell) method.
If I write the function without ArrayFormula like this:
On C1, =retValue(address(row(C1),column(A:A),4))
On C2, =retValue(address(row(C2),column(A:A),4))
On C3, =retValue(address(row(C3),column(A:A),4))
I get the expected result:
| A | B | C |
1| a | A1| a |
2| b | A2| b |
3| c | A3| c |
So, how to make it work in ArrayFormula?
Issue:
SpreadsheetApp.getActive().getRange(cell)
cell is array if you provide a array input. getRange method expects a single string as input.
Solution:
map the array to single value
References:
Custom Function#Optimization
Best practices
Snippet#1:
function retValue(cell){
if(cell.map) {
return cell.map(retValue);
} else {
var cellRang = SpreadsheetApp.getActive().getRange(cell);
return cellRang.getValue();
}
}
Snippet#2:
Note that in the previous snippet you're calling getValue() 1 time per each cell in the input array. This is extremely slow. Better way is to call it as a batch:
=retValues("A1:B4")
function retValues(cell){//modified
var cellRang = SpreadsheetApp.getActive().getRange(cell);
return cellRang.getValues();//modified
}
Note that:
Only 1 call to getValues() is made.
Formula returns a array without explicit use of =ARRAYFORMULA(). All custom formulas are by default, array formulas, but they need to be configured to return values as arrays in apps script.

Pyspark - getting values from an array that has a range of min and max values

I'm trying to write a query in PySpark that will get the correct value from an array.
For example, I have dataframe called df with three columns, 'companyId', 'companySize' and 'weightingRange'. The 'companySize' column is just the number of employees. The column 'weightingRange' is an array with the following in it
[ {"minimum":0, "maximum":100, "weight":123},
{"minimum":101, "maximum":200, "weight":456},
{"minimum":201, "maximum":500, "weight":789}
]
so the dataframe looks like this (weightingRange is as above, its truncated in the below example for clearer formating)
+-----------+-------------+------------------------+--+
| companyId | companySize | weightingRange | |
+-----------+-------------+------------------------+--+
| ABC1 | 150 | [{"maximum":100, etc}] | |
| ABC2 | 50 | [{"maximum":100, etc}] | |
+-----------+-------------+------------------------+--+
So for a entry for company size = 150 I need to return the weight 456 into a column called 'companyWeighting'
So it should show the following
+-----------+-------------+------------------------+------------------+
| companyId | companySize | weightingRange | companyWeighting |
+-----------+-------------+------------------------+------------------+
| ABC1 | 150 | [{"maximum":100, etc}] | 456 |
| ABC2 | 50 | [{"maximum":100, etc}] | 123 |
+-----------+-------------+------------------------+------------------+
I've had a look at
df.withColumn("tmp",explode(col("weightingRange"))).select("tmp.*")
and then joining but trying to apply that would Cartesian the data.
Suggestions appreciated!
You can approach like this,
First creating a sample dataframe,
import pyspark.sql.functions as F
df = spark.createDataFrame([
('ABC1', 150, [ {"min":0, "max":100, "weight":123},
{"min":101, "max":200, "weight":456},
{"min":201, "max":500, "weight":789}]),
('ABC2', 50, [ {"min":0, "max":100, "weight":123},
{"min":101, "max":200, "weight":456},
{"min":201, "max":500, "weight":789}])],
['companyId' , 'companySize', 'weightingRange'])
Then, creating a udf function and applying it on each row to get the new column,
def get_weight(wt,wt_rnge):
for _d in wt_rnge:
if _d['min'] <= wt <= _d['max']:
return _d['weight']
get_weight_udf = F.udf(lambda x,y: get_weight(x,y))
df = df.withColumn('companyWeighting', get_weight_udf(F.col('companySize'), F.col('weightingRange')))
df.show()
You get the output as,
+---------+-----------+--------------------+----------------+
|companyId|companySize| weightingRange|companyWeighting|
+---------+-----------+--------------------+----------------+
| ABC1| 150|[Map(weight -> 12...| 456|
| ABC2| 50|[Map(weight -> 12...| 123|
+---------+-----------+--------------------+----------------+

Google script Multiple criteria in same row and validation of value in previous row

I'm starting with a script that someone here graciously helped with and need to build onto it and do not know where to start. Here is the current script:
function yourFunction(){
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName('Sheet1');
var rg=sh.getDataRange();//columns are fruit,status and then cost.
var vA=rg.getValues();
for(var i=1;i<vA.length;i++){
if(vA[i][0].toString()=='Apple' && vA[i][1].toString()=='Ripe' && vA[i][2].toString=='Large' && vA[i][4].toString=''){
vA[i][4]=5.5;
}
}
rg.setValues(vA);//This writes all of the data at one time.
}
What I would like to add to this is a second set of criteria that looks at another column value = Lot Number(Column D). Assuming that the current Lot Number is the same as the previous row's and where all the above match, each additional rows will be a set value 3. But if the value of the Lot Number before the current row is not the same, then the value is 5. In what I've read, there may need to be some looping condition in this so the calculations don't keep going on and on. Any help here would be much appreciated. Thanks!
Here is a link to a basic format of the spreadsheet Test Script
Here's an updated function that demonstrates the general approach for what I believe you're describing.
var COLUMNS = {
FRUIT: 0,
STATUS: 1,
SIZE: 2,
LOT_NUMBERr: 3,
COST: 4,
NEW_VALUE: 5,
}
function updateValues() {
var sheet = SpreadsheetApp.getActive().getSheetByName('Sheet3');
var range = sheet.getDataRange();
var values = range.getValues();
var previousLotNumber = -1;
for(var i = 1; i < values.length; i++){
if (values[i][COLUMNS.FRUIT] == 'Apple'
&& values[i][COLUMNS.STATUS] == 'Ripe') {
values[i][COLUMNS.COST] = 5.5;
}
if (previousLotNumber == values[i][COLUMNS.LOT_NUMBER]) {
values[i][COLUMNS.NEW_VALUE] = 3;
} else {
values[i][COLUMNS.NEW_VALUE] = 5;
}
previousLotNumber = values[i][COLUMNS.LOT_NUMBER];
}
range.setValues(values);
}
After running this function, this sheet looks like the following:
+--------+-----------+--------+------------+------+-----------+
| Fruit | Status | Size | Lot Number | Cost | New Value |
+--------+-----------+--------+------------+------+-----------+
| Apple | Ripe | Large | 101 | 5.5 | 5 |
| Apple | Ripe | Medium | 101 | 5.5 | 3 |
| Apple | Ripe | Large | 103 | 5.5 | 5 |
| Apple | Not Ready | Large | 102 | | 5 |
| Apple | Not Ready | Medium | 101 | | 5 |
| Banana | Ripe | Large | 201 | | 5 |
| Orange | Ripe | Large | 301 | | 5 |
| Orange | Not Ready | Medium | 301 | | 3 |
| Pear | Ripe | Large | 401 | | 5 |
+--------+-----------+--------+------------+------+-----------+
A few notes:
use descriptive variable names
use a set of constants to provide descriptive names for the columns
for your ask, all you need to do is use a variable to store the previous rows lot number, no additional looping complexity required

Create new row for each not empty column

I'm trying to convert an Excel macro to Google Apps Script. I would like to create a new row on a specific sheet for each not empty column in Google Spreadsheets.
My Inputsheet looks like the following:
ID | Inrellevant Column | Givenmoney | Takenmoney | Othermoney
1 | Data1 | 100 | 200 | 300
2 | Data2 | 400 | | 500
I want to create a new row in another sheet for each not empty cell, so the desired Outputsheet would be:
ID | Inrellevant Column | Moneycode | Amount
1 | Data1 | Givenmoney | 100
1 | Data1 | Takenmoney | 200
1 | Data1 | Othermoney | 300
2 | Data2 | Givenmoney | 400
2 | Data2 | Othermoney | 500
I tried the following:
Outputsheet.getRange('A2').offset(0, 0, Inputsheet.length).setValues(Inputsheet);
However I can't see to create a loop to create new rows for each not empty column.
Hoi Fred, assuming you want the output to appear from the top left cell onwards in the sheet 'Outputsheet', try this code:
function myFunction() {
var ss = SpreadsheetApp.getActive(),
source = ss.getSheetByName('Inputsheet'),
target = ss.getSheetByName('Outputsheet'),
arr = [
["ID", "Header 2nd col", "Moneycode", "Amount"]
],
data = source.getDataRange().getValues(),
headers = data[0];
data.splice(1)
.forEach(function (r) {
r.forEach(function (c, i) {
if (!isNaN(parseFloat(c)) && isFinite(c) && i > 1) {
arr.push([r[0], r[1], headers[i], c])
}
})
})
target.getRange(1, 1, arr.length, arr[0].length).setValues(arr);
}
See this example sheet where you can run the above script from the menu 'My Menu'....