How to use a custom function with an ArrayFormula - google-apps-script

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.

Related

Google-sheet: select related cell from imported range

I have the following google-sheet:
| | A | B | C | D |
| 1| | Item1 | Item2 | Item3 |
| 2| Value1 | yes | no | no |
| 3| Value2 | no | no | yes |
| 4| Value3 | no | yes | no |
I need to import specific data from this sheet into another range so result should contain values from A, which have "yes" between B:E, and related item from the first row, like:
Value1 Item1
Value2 Item3
Value3 Item2
I can import using query and condition for "yes", but no ideas how to read related cell with Item1, Item2.. from first row above:
=query({importrange("my_range_id", "Data!A1:Z999"}, "select Col1 where Col2='yes' or Col3='yes' or Col4='yes'")
Thanks for help in advance!
May be you can first import data, then retrieve the column corresponding to 'yes' by this formula using matrix multiplication
={A2:A5,arrayformula(mmult((if(B2:D5="yes",1,0)),transpose(column(B1:D1)-1)))}
and then retrieve the header of the column
=offset($A$1,,G2)
As an addition, in case you still want to use a query:
Try this in F2:
=ARRAYFORMULA(QUERY(SPLIT(TRANSPOSE(SPLIT(QUERY(TRANSPOSE(QUERY(TRANSPOSE(
IF(A2:A<>"",
"♦"&A2:A&"😊"&B1:D1&"😊"&B2:D, ))
,,999^99)),,999^99), "♦")), "😊"),"Select Col1,Col2 where Col3 contains 'YES'"))
Here is an integrated formula
={A2:A,arrayformula(vlookup(mmult((if($B$2:$D="yes",1,0)),transpose(column($B$1:$D$1)-1)),{arrayformula(transpose(COLUMN(A1:D1)-1)),transpose(A1:D1)},2,0))}

Why isn't array Intersection in google spreadsheet working as expected?

This is my first time using google spreadsheets, and I'm trying to accomplish something in the functions. I know it's javascript (A language I'm not entirely familiar with). I'm trying to take a range from two tables, and get the intersection location of each, and fill in missing values for rows that already should exist.
For example, assuming the following two sheets:
Sheet1
+-----------+----------+--------+---------+
| Fruit | Color | Weight | isApple |
+-----------+----------+--------+---------+
| Banana | Yellow | 3 | no |
| Orange | Orange | 3 | no |
| Apple | Red | 2 | yes |
| Pineapple | Brownish | 5 | no |
+-----------+----------+--------+---------+
Sheet2
+-----------+----------+--------+---------+
| Fruit | Color | Weight | isApple |
+-----------+----------+--------+---------+
| Banana | | | |
| Apple | | | |
| Pear | | | |
| Watermelon| | | |
+-----------+----------+--------+---------+
I want to find the intersections of the Fruit row, and fill in the color, weight, and isApple, of each one we know.
I wrote something I felt confident should work to get the intersection of range1, and it returns an empty array for some reason. When I test this in Apps Script Editor, it seems to work fine.
//assuming range1 = Sheet1!A2:A5 and range2=Sheet2!A2:A5
function intersection(range1, range2) {
var i = 0;
var j = 0;
var matches = new Array();
while(i < range1.length){
if(range2.toString().includes(range1[i].toString())){
matches.push(i);
}
i++
}
return matches;
}
I would expect this to return an array of [0,2] since the 0th element Banana, and the 1st element Apple from Sheet1, exist in sheet 2.
I would then use that data to fill in the rows for Apple, and Banana from the information in Sheet2.
I'm not at that second part yet, since I can't seem to get the sheet to even find the intersection.
My end goal expected output would be that Sheet 2 is changed to:
Sheet2
+-----------+----------+--------+---------+
| Fruit | Color | Weight | isApple |
+-----------+----------+--------+---------+
| Banana | Yellow | 3 | no |
| Apple | Red | 2 | yes |
| Pear | | | |
| Watermelon| | | |
+-----------+----------+--------+---------+
I believe your goal as follows.
You want to achieve the result from "Sheet1" and "Sheet2" as shown in your question.
You want to achieve this using the custom function.
For this, how about this answer?
Modification points:
In your script, I thought that you are using the custom function of =intersection(Sheet1!A2:A5,Sheet2!A2:A5). In this case, the arguments of range1 and range2 of function intersection(range1, range2) { are [["Banana"],["Orange"],["Apple"],["Pineapple"]] and [["Banana"],["Apple"],["Pear"],["Watermelon"]], which are 2 dimensional arrays, respectively. Namely, the values on the sheet are sent to the arguments. In order to achieve your goal, I would like to propose to use the custom function like =intersection(Sheet1!A2:D5,Sheet2!A2:A5). By this, the values from "Sheet1" can be used in the custom function.
In your script, the value of [0,2] is retrieved as matches. This is the indexes of range1. But in this case, it is required to also know the indexes matching to range2. This has already mentioned in your question. In this case, how about the following flow?
In order to achieve your goal, I would like to propose the following flow.
From the values of "Sheet1", create an object for searching values of the column "A".
From the values of "Sheet2", create an result array using the created object.
The sample script reflected above flow is as follows.
Sample script:
Please copy and paste the following script, and out the custom function =intersection(Sheet1!A2:D5,Sheet2!A2:A5) to the cell "B2" of "Sheet2". By this, the result that you showed at the bottom of your question is obtained.
function intersection(values1, values2) {
const obj = values1.reduce((o, [a, ...bcd]) => Object.assign(o, {[a]: bcd}), {});
return values2.map(([a]) => obj[a] ? obj[a] : [""]);
}
Result:
References:
Custom Functions in Google Sheets
reduce()
Object.assign()
Spread syntax (...)
map()

Eiffel library: JSON_OBJECT.item and common method to get content

If I want to have the content value as string of a JSON_OBJECT.item ("key") without having to
some_json_value_as_string: STRING
do
if attached {JSON_STRING} l_json_o as l_s then
Result := l_s.unescaped_string_8
elseif attached {JSON_NUMBER} l_json_o as l_n then
Result := l_n.item.out
else
check
you_forgot_to_treat_a_case: False
end
end
end
for a json object like
{
| | "datasource_name": "DODBC",
| | "datasource_username": "dev_db_usr",
| | "datasource_password": "somePassword",
| | "ewf_listening_port": 9997,
| | "log_file_path": "/var/log/ewf_app.log",
| | "default_selected_company": 1,
| | "default_selected_branch": 1,
| | "default_selected_consumption_sector": 1,
| | "default_selected_measuring_point": 1,
| | "default_selected_charge_unit": -1
| }
the {JSON_VALUE}.representation with io.putstring is:
datasource_username=dev_db_usr
and not the value only!!!
is there a way to do that? I didn't find intuitive the different methods of JSON_VALUE: values as the out method gives the class and pointer address, which is really far from a string representation of the associated json object for me...
The feature {JSON_VALUE}.representation is the string representation of the Current JSON value.
Ok, but if you have jo: JSON_OBJECT and then suppose you have datasource_username_key: STRING = "datasource_username"
You can do
if attached jo.item (datasource_username_key) as l_value then
print (l_value.representation)
end

How to pass a cell reference to an Apps Script custom function?

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]]

Extended Metadata

The meta function in kdb/q returns the following info about the table:
c – (symbol) column names
t – (char) data type
f – (symbol) domain of foreign keys
a - (symbol) attributes.
I would like to extend this to include more information about the table. The specific case that I am trying to solve is to include the timezone information about the time data columns in the table.
For example:
select from Price
+-------------------------+-------------------------+--------+-------+
| Time | SysTime | Ticker | Price |
+-------------------------+-------------------------+--------+-------+
| 2016.09.15D09:18:02.391 | 2016.09.15D08:18:02.391 | IBM | 63.46 |
| 2016.09.15D09:18:02.491 | 2016.09.15D08:16:22.391 | MSFT | 96.72 |
| 2016.09.15D09:18:02.591 | 2016.09.15D08:14:42.391 | AAPL | 23.06 |
+-------------------------+-------------------------+--------+-------+
meta Price
+---------+---+---+---+
| c | t | f | a |
+---------+---+---+---+
| Time | p | | |
| SysTime | p | | |
| Ticker | s | | |
| Price | f | | |
+---------+---+---+---+
I would like to have additional info about the time data columns (Time and SysTime) in the meta.
For Example, something like this:
metaExtended Price
+---------+---+---+---+------------------+
| c | t | f | a | z |
+---------+---+---+---+------------------+
| Time | p | | | America/New_York |
| SysTime | p | | | America/Chicago |
| Ticker | s | | | |
| Price | f | | | |
+---------+---+---+---+------------------+
Please note that I have a function that takes in the table and column to return the time zone.
TimeZone[Price;Time] returns America/New_York
My question is only about how to include this information in the meta function. The second question that I have is that if the user does something like this, newPriceTable:Price (creating a new table which is the same as the previous table) then the metaExtended function should return the same value for both the tables (akin to calling a function on two different variables having the same object reference)
Does something similar exist in sql?
meta is a reserved word and therefore cannot be redefined. But you can create your own implementation and use it in place of meta:
TimeZone:{[Table;Col] ... } / your TimeZone function
metaExtended:{meta[x],'([]z:TimeZone[t]each cols x)}
metaExtended Price
Regarding your second question, I don't think it's possible to do what you want in k/q. Immediately after assigning Price to newPriceTable the latter is indeed a reference, but as soon as you modify it kdb will create a copy and modify it instead of the original. The problem is there is no way to tell whether newPriceTable is still a reference to Price or a fresh new object.
You can use lj to join them into one metaExtended function.
The function will check for all the time cols and run TimeZone function on them and join the result with meta result:
metaExtended:{[tbl] meta[tbl] lj 1!select c,z:TimeZone[tbl] each t from meta[tbl] where t in "tp"}
metaExtended `t
when you assign this table to new variable it will be assigned as a reference.
nt:t / nt and t pointing to same object
Yo can check the reference count of a variable using -16! .
-16!t
At this point metaExtended function will give same output. But once some update is done on any of these variables pointing to same table, kdb will create a new copy for updated table/variable. From this point they are 2 different objects. Now output of metaExtended function depends on the object schema.