How to compute the Google Query's data field? - google-sheets

how can I compute QUERY's data attribute to make data range dynamic?
More details: I have a spreadsheet with multiple worksheets incl. a separate "summary" one. Here, a couple of queries are aggregating data from these multiple sheets. All fine. But: any time I add/remove a sheet, I will have to update all queries manually - which is quite cumbersome. So I created a small script that returns the names of all worksheets. Nice.
What I have trouble with: how can I make QUERY() accept these worksheet names? It returns #value.
originally:= QUERY({'sheet 1'!$A:$C; 'sheet 2'!$A:$C}; "SELECT SUM(Col2), ...") //works.
with script:= QUERY(getSheetNames(); "SELECT SUM(Col2), ...") //#value error
add script to cell and referencing cell:= QUERY(A1; "SELECT SUM(Col2), ...") //#value error
custom function:getSheetNames() currently returns "{'sheet 1'!$A:$C; 'sheet 2'!$A:$C}"
function getSheetNames() {
var out = "";
var columns = "!$A:$C";
var apostrophe = "'";
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i = 1 ; i < sheets.length; i++) {
out += apostrophe + sheets[i].getName() + apostrophe + columns + "; ";
}
out = "{" + out.slice(0, -2) + "}";
return out;
}
Many thanks for help!

Related

Advance manual unpivot of data table [duplicate]

I am trying to produce a "reverse pivot" function. I have searched long and hard for such a function, but cannot find one that is already out there.
I have a summary table with anywhere up to 20 columns and hundreds of rows, however I would like to convert it into a flat list so I can import to a database (or even use the flat data to create more pivot tables from!)
So, I have data in this format:
Customer 1
Customer 2
Customer 3
Product 1
1
2
3
Product 2
4
5
6
Product 3
7
8
9
And need to convert it to this format:
Customer | Product | Qty
-----------+-----------+----
Customer 1 | Product 1 | 1
Customer 1 | Product 2 | 4
Customer 1 | Product 3 | 7
Customer 2 | Product 1 | 2
Customer 2 | Product 2 | 5
Customer 2 | Product 3 | 8
Customer 3 | Product 1 | 3
Customer 3 | Product 2 | 6
Customer 3 | Product 3 | 9
I have created a function that will read the range from sheet1 and append the re-formatted rows at the bottom of the same sheet, however I am trying to get it working so I can have the function on sheet2 that will read the whole range from sheet1.
No matter what I try, I can't seem to get it to work, and was wondering if anybody could give me any pointers?
Here is what I have so far:
function readRows() {
var sheet = SpreadsheetApp.getActiveSheet();
var rows = sheet.getDataRange();
var numRows = rows.getNumRows();
var values = rows.getValues();
heads = values[0]
for (var i = 1; i <= numRows - 1; i++) {
for (var j = 1; j <= values[0].length - 1; j++) {
var row = [values[i][0], values[0][j], values[i][j]];
sheet.appendRow(row)
}
}
};
I wrote a simple general custom function, which is 100% reusable you can unpivot / reverse pivot a table of any size.
In your case you could use it like this: =unpivot(A1:D4,1,1,"customer","sales")
So you can use it just like any built-in array function in spreadsheet.
Please see here 2 examples:
https://docs.google.com/spreadsheets/d/12TBoX2UI_Yu2MA2ZN3p9f-cZsySE4et1slwpgjZbSzw/edit#gid=422214765
The following is the source:
/**
* Unpivot a pivot table of any size.
*
* #param {A1:D30} data The pivot table.
* #param {1} fixColumns Number of columns, after which pivoted values begin. Default 1.
* #param {1} fixRows Number of rows (1 or 2), after which pivoted values begin. Default 1.
* #param {"city"} titlePivot The title of horizontal pivot values. Default "column".
* #param {"distance"[,...]} titleValue The title of pivot table values. Default "value".
* #return The unpivoted table
* #customfunction
*/
function unpivot(data,fixColumns,fixRows,titlePivot,titleValue) {
var fixColumns = fixColumns || 1; // how many columns are fixed
var fixRows = fixRows || 1; // how many rows are fixed
var titlePivot = titlePivot || 'column';
var titleValue = titleValue || 'value';
var ret=[],i,j,row,uniqueCols=1;
// we handle only 2 dimension arrays
if (!Array.isArray(data) || data.length < fixRows || !Array.isArray(data[0]) || data[0].length < fixColumns)
throw new Error('no data');
// we handle max 2 fixed rows
if (fixRows > 2)
throw new Error('max 2 fixed rows are allowed');
// fill empty cells in the first row with value set last in previous columns (for 2 fixed rows)
var tmp = '';
for (j=0;j<data[0].length;j++)
if (data[0][j] != '')
tmp = data[0][j];
else
data[0][j] = tmp;
// for 2 fixed rows calculate unique column number
if (fixRows == 2)
{
uniqueCols = 0;
tmp = {};
for (j=fixColumns;j<data[1].length;j++)
if (typeof tmp[ data[1][j] ] == 'undefined')
{
tmp[ data[1][j] ] = 1;
uniqueCols++;
}
}
// return first row: fix column titles + pivoted values column title + values column title(s)
row = [];
for (j=0;j<fixColumns;j++) row.push(fixRows == 2 ? data[0][j]||data[1][j] : data[0][j]); // for 2 fixed rows we try to find the title in row 1 and row 2
for (j=3;j<arguments.length;j++) row.push(arguments[j]);
ret.push(row);
// processing rows (skipping the fixed columns, then dedicating a new row for each pivoted value)
for (i=fixRows; i<data.length && data[i].length > 0; i++)
{
// skip totally empty or only whitespace containing rows
if (data[i].join('').replace(/\s+/g,'').length == 0 ) continue;
// unpivot the row
row = [];
for (j=0;j<fixColumns && j<data[i].length;j++)
row.push(data[i][j]);
for (j=fixColumns;j<data[i].length;j+=uniqueCols)
ret.push(
row.concat([data[0][j]]) // the first row title value
.concat(data[i].slice(j,j+uniqueCols)) // pivoted values
);
}
return ret;
}
That is basically array manipulation... below is a code that does what you want and writes back the result below existing data.
You can of course adapt it to write on a new sheet if you prefer.
function transformData(){
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getDataRange().getValues();//read whole sheet
var output = [];
var headers = data.shift();// get headers
var empty = headers.shift();//remove empty cell on the left
var products = [];
for(var d in data){
var p = data[d].shift();//get product names in first column of each row
products.push(p);//store
}
Logger.log('headers = '+headers);
Logger.log('products = '+products);
Logger.log('data only ='+data);
for(var h in headers){
for(var p in products){ // iterate with 2 loops (headers and products)
var row = [];
row.push(headers[h]);
row.push(products[p]);
row.push(data[p][h])
output.push(row);//collect data in separate rows in output array
}
}
Logger.log('output array = '+output);
sheet.getRange(sheet.getLastRow()+1,1,output.length,output[0].length).setValues(output);
}
to automatically write the result in a new sheet replace last line of code with these :
var ns = SpreadsheetApp.getActive().getSheets().length+1
SpreadsheetApp.getActiveSpreadsheet().insertSheet('New Sheet'+ns,ns).getRange(1,1,output.length,output[0].length).setValues(output);
google-sheets-formula
With the advent of new LAMBDA and MAKEARRAY functions, we can unpivot the data without string manipulation. This works by creating a sequence of appropriate index numbers for the new array, which should be faster than string manipulation.
=ARRAYFORMULA(LAMBDA(range,s_cols,
QUERY(
MAKEARRAY(ROWS(range)*(COLUMNS(range)-s_cols),s_cols+1,
LAMBDA(i,j,
TO_TEXT(
INDEX(range,
ROUNDDOWN(1+(i-1)/(COLUMNS(range)-s_cols)),
if(j>s_cols,MOD(i-1,COLUMNS(range)-s_cols)+s_cols+1,j)
)
)
)
),"where Col"&s_cols+1&" is not null"
)
)(A1:C10,2))
Or as a named function(UNPIVOT(range,s_cols)):
=ARRAYFORMULA(
QUERY(
MAKEARRAY(ROWS(range)*(COLUMNS(range)-s_cols),s_cols+1,
LAMBDA(i,j,
TO_TEXT(
INDEX(range,
ROUNDDOWN(1+(i-1)/(COLUMNS(range)-s_cols)),
if(j>s_cols,MOD(i-1,COLUMNS(range)-s_cols)+s_cols+1,j)
)
)
)
),"where Col"&s_cols+1&" is not null"
)
)
Arguments:
range: The range to unpivot. Eg:A1:C10
s_cols: The number of static columns on the left.Eg:2
google-apps-script
Using simple, yet powerful loops on V8 engine:
/**
* Unpivots the given data
*
* #return Unpivoted data from array
* #param {A1:C4} arr 2D Input Array
* #param {1=} ignoreCols [optional] Number of columns on the left to ignore
* #customfunction
*/
const unpivot = (arr, ignoreCols = 1) =>
((j, out) => {
while (++j < arr[0].length)
((i) => {
while (++i < arr.length)
out.push([arr[0][j], ...arr[i].slice(0, ignoreCols), arr[i][j]]);
})(0);
return out;
})(ignoreCols - 1, []);
Usage:
=UNPIVOT(A1:C4)
=UNPIVOT(A1:F4,3)//3 static cols on left
={{"Customer","Products","Qty"};UNPIVOT(A1:D4)}//add headers
Live demo:
/*<ignore>*/console.config({maximize:true,timeStamps:false,autoScroll:false});/*</ignore>*/
const arr = [
[' ', ' Customer 1 ', ' Customer 2 ', ' Customer 3'],
['Product 1 ', ' 1 ', ' 2 ', ' 3'],
['Product 2 ', ' 4 ', ' 5 ', ' 6'],
['Product 3 ', ' 7 ', ' 8 ', ' 9'],
];
console.log("Input table")
console.table(arr)
/**
* Unpivots the given data
*
* #return Unpivoted data from array
* #param {A1:C4} arr 2D Input Array
* #param {1=} ignoreCols [optional] Number of columns on the left to ignore
* #customfunction
*/
const unpivot = (arr, ignoreCols = 1) =>
((j, out) => {
while (++j < arr[0].length)
((i) => {
while (++i < arr.length)
out.push([arr[0][j], ...arr[i].slice(0, ignoreCols), arr[i][j]]);
})(0);
return out;
})(ignoreCols - 1, []);
console.log("Output table")
console.table(unpivot(arr));
console.log("Output table with 2 static columns")
console.table(unpivot(arr,2));
<!-- https://meta.stackoverflow.com/a/375985/ --> <script src="https://gh-canon.github.io/stack-snippet-console/console.min.js"></script>
Check history for older deprecated functions
Use FLATTEN. It converts any array into single column.
Here's the formula for unpivot:
=ARRAYFORMULA(SPLIT(FLATTEN(A2:A12&"💣"&B1:F1&"💣"&B2:F12),"💣"))
FLATTEN creates 1-column array of Item1💣Date1💣67455 strings, which we then split.
Please copy the sample file to try.
Shorter:
=index(SPLIT(FLATTEN(A2:A12&"💣"&B1:F1&"💣"&B2:F12),"💣"))
Please also see this solution.
It uses INDIRECT and settings, so the formula looks like a more general solution:
I didn't think you had enough array formula answers so here's another one.
Test Data (Sheet 1)
Formula for customer
=ArrayFormula(hlookup(int((row(indirect("1:"&Tuples))-1)/Rows)+2,{COLUMN(Sheet1!$1:$1);Sheet1!$1:$1},2))
(uses a bit of math to make it repeat and hlookup to find correct column in column headers)
Formula for product
=ArrayFormula(vlookup(mod(row(indirect("1:"&Tuples))-1,Rows)+2,{row(Sheet1!$A:$A),Sheet1!$A:$A},2))
(similar approach using mod and vlookup to find correct row in row headers)
Formula for quantity
=ArrayFormula(vlookup(mod(row(indirect("1:"&Tuples))-1,Rows)+2,{row(Sheet1!$A:$A),Sheet1!$A:$Z},int((row(indirect("1:"&Tuples))-1)/Rows)+3))
(extension of above approach to find both row and column in 2d array)
Then combining these three formulas into a query to filter out any blank values for quantity
=ArrayFormula(query(
{hlookup(int((row(indirect("1:"&Tuples))-1)/Rows)+2, {COLUMN(Sheet1!$1:$1);Sheet1!$1:$1},2),
vlookup(mod(row(indirect("1:"&Tuples))-1,Rows)+2,{row(Sheet1!$A:$A),Sheet1!$A:$A},2),
vlookup(mod(row(indirect("1:"&Tuples))-1,Rows)+2,{row(Sheet1!$A:$A),Sheet1!$A:$Z},int((row(indirect("1:"&Tuples))-1)/Rows)+3)},
"select * where Col3 is not null"))
Note
The named ranges Rows and Cols are obtained from the first column and row of the data using counta and Tuples is their product. The separate formulas
=counta(Sheet1!A:A)
=counta(Sheet1!1:1)
and
=counta(Sheet1!A:A)*counta(Sheet1!1:1)
could be included in the main formula if required with some loss of readability.
For reference, here is the 'standard' split/join solution (with 50K data limit) adapted for the present situation:
=ArrayFormula(split(transpose(split(textjoin("♫",true,transpose(if(Sheet1!B2:Z="","",Sheet1!B1:1&"♪"&Sheet1!A2:A&"♪"&Sheet1!B2:Z))),"♫")),"♪"))
This is also fairly slow (processing 2401 array elements). If you restrict the computation to the actual dimensions of the data, it is much faster for small datasets:
=ArrayFormula(split(transpose(split(textjoin("♫",true,transpose(if(Sheet1!B2:index(Sheet1!B2:Z,counta(Sheet1!A:A),counta(Sheet1!1:1))="","",Sheet1!B1:index(Sheet1!B1:1,counta(Sheet1!1:1))&"♪"&Sheet1!A2:index(Sheet1!A2:A,counta(Sheet1!A:A))&"♪"&Sheet1!B2:index(Sheet1!B2:Z,counta(Sheet1!A:A),counta(Sheet1!1:1))))),"♫")),"♪"))
=ARRAYFORMULA({"Customer", "Product", "Qty";
QUERY(TRIM(SPLIT(TRANSPOSE(SPLIT(TRANSPOSE(QUERY(TRANSPOSE(QUERY(TRANSPOSE(
IF(B2:Z<>"", B1:1&"♠"&A2:A&"♠"&B2:Z&"♦", )), , 999^99)), , 999^99)), "♦")), "♠")),
"where Col1<>'' order by Col1")})
Here another alternative:
=arrayformula
(
{ "PRODUCT","CUSTOMER","QTY";
split
( transpose ( split
( textjoin("✫" ,false,filter(Sheet2!A2:A,Sheet2!A2:A<>"") & "✤" &
filter(Sheet2!B1:1,Sheet2!B1:1<>""))
,"✫",true,false)),"✤",true,false
),
transpose ( split ( textjoin ( "✤", false, transpose ( filter
(
indirect( "Sheet2!B2:" & MID(address(1,COUNTA( Sheet2!B1:1)+1), 2,
FIND("$",address(1,COUNTA( Sheet2!B1:1)+1),2)-2)
)
, Sheet2!A2:A<>""
))),"✤",true,false)
)
}
)
Explanation:
1. "PRODUCT","CUSTOMER","QTY"
-- Use for giving title
2. split
( transpose ( split
( textjoin("✫" ,false,filter(Sheet2!A2:A,Sheet2!A2:A<>"") & "✤" &
filter(Sheet2!B1:1,Sheet2!B1:1<>""))
,"✫",true,false)),"✤",true,false
)
-- Use for distributing Row1 and ColumnA, to be Product and Customer Columns
3. transpose ( split ( textjoin ( "✤", false, transpose ( filter
(
indirect( "Sheet2!B2:" & MID(address(1,COUNTA( Sheet2!B1:1)+1), 2,
FIND("$",address(1,COUNTA( Sheet2!B1:1)+1),2)-2)
)
, Sheet2!A2:A<>""
))),"✤",true,false)
)
--use to distributed data qty to Qty Column
Sheet2 Pict:
Result Sheet Pict:
Input Sheet
This function will handle many customers and many products and it will sum the quantities of multiple customer/product entries and summarize it into one simple table.
The Code:
function rPVT() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName('Sheet1');
var osh=ss.getSheetByName('Sheet2');
osh.clearContents();
var vA=sh.getDataRange().getValues();
var itoh={};
var pObj={};
vA[0].forEach(function(h,i){if(h){itoh[i]=h;}});
for(var i=1;i<vA.length;i++) {
for(var j=1;j<vA[i].length;j++) {
if(!pObj.hasOwnProperty(itoh[j])){pObj[itoh[j]]={};}
if(!pObj[itoh[j]].hasOwnProperty(vA[i][0])){pObj[itoh[j]][vA[i][0]]=vA[i][j];}else{pObj[itoh[j]][vA[i][0]]+=(vA[i][j]);}
}
}
var oA=[['Customer','Product','Quantity']];
Object.keys(pObj).forEach(function(ik){Object.keys(pObj[ik]).forEach(function(jk){oA.push([ik,jk,pObj[ik][jk]]);});});
osh.getRange(1,1,oA.length,oA[0].length).setValues(oA);
}
Output Sheet:
The following function reads Sheet2 which is the output of the above function and returns it to the original format.
function PVT() {
var ss=SpreadsheetApp.getActive();
var sh2=ss.getSheetByName('Sheet2');
var sh3=ss.getSheetByName('Sheet3');
sh3.clearContents();
var vA=sh2.getRange(2,1,sh2.getLastRow()-1,sh2.getLastColumn()).getValues();
pObj={};
vA.forEach(function(r,i){if(!pObj.hasOwnProperty(r[1])){pObj[r[1]]={};}if(!pObj[r[1]].hasOwnProperty(r[0])){pObj[r[1]][r[0]]=r[2];}else{pObj[r[1]][r[0]]+=r[2];}});
var oA=[];
var ikeys=Object.keys(pObj);
var jkeys=Object.keys(pObj[ikeys[0]]);
var hkeys=jkeys.slice();
hkeys.unshift('');
oA.push(hkeys);
ikeys.forEach(function(ik,i){var row=[];row.push(ik);jkeys.forEach(function(jk,j){row.push(pObj[ik][jk]);});oA.push(row);});
sh3.getRange(1,1,oA.length,oA[0].length).setValues(oA);
}
If your data has a single unique key column, this spreadsheet may have what you need.
Your unpivot sheet will contain:
The key column =OFFSET(data!$A$1,INT((ROW()-2)/5)+1,0)
The column header column =OFFSET(data!$A$1,0,IF(MOD(ROW()-1,5)=0,5,MOD(ROW()-1,5)))
The cell value column =INDEX(data!$A$1:$F$100,MATCH(A2,data!$A$1:$A$100,FALSE),MATCH(B2,data!$A$1:$F$1,FALSE))
where 5 is the number of columns to unpivot.
I did not make the spreadsheet. I happened across it in the same search that led me to this question.
One range refrence
This will work regardless of the number of customers and products. with one range reference in this case (A1:D4)
=ArrayFormula({SPLIT("Customer|Product|Qty","|");
QUERY(LAMBDA(r,SPLIT(FLATTEN(
QUERY({r}, " Select Col1 ", 1)&"+"&
QUERY({r}, " select "& TEXTJOIN(",",1,REGEXREPLACE("Col#", "#", SEQUENCE(COLUMNS(QUERY(r, " select * limit 0 ", 1))-1,1,2,1)&""))&" limit 0 ", 1)&"+"&
QUERY({QUERY({r}, " Select "& TEXTJOIN(",",1,REGEXREPLACE("Col#", "#", SEQUENCE(COLUMNS(QUERY({r}, " select * where Col1 <> '' ", 1))-1,1,2,1)&""))&" ", 0)},
" Select * where Col1 is not null ")),"+"))(A1:D4)," Select * Where Col2 <> '' ")})
Demonstration
This woks well when you have this table "on the left" as an output of another formula.
in this case simulated with the range A1:G15
20 columns and hundreds of rows
Named function
Pending...
Used formulas help
ARRAYFORMULA - SPLIT - QUERY - LAMBDA - FLATTEN - TEXTJOINREGEXREPLACE - SEQUENCE - COLUMNS - NOT

How can I combine multiple functions with ArrayFormula

I've the below formula using ImportRange and Query along with Join and Split working correctly:
=join(" / ", QUERY(IMPORTRANGE("Google-Sheet-ID","RawData!A:AC"),"select Col25 where Col1 = " & JOIN(" OR Col1 = ", split(V2:V,"+")), 0))
Also, I've the below ArrayFormula with Split function working smoothly:
=ARRAYFORMULA(if(len(V2:V)=0,,split(V2:V,"+")))
But When I tried combining them together using the below formula:
=ARRAYFORMULA(if(len(V2:V)=0,,join(" / ", QUERY(IMPORTRANGE("Google-Sheet-ID","RawData!A:AC"),"select Col25 where Col1 = " & JOIN(" OR Col1 = ", split(V2:V,"+")), 0))))
It failed, and gave me the below error:
Error
Function SPLIT parameter 1 value should be non-empty.
Here is my sheet for your testing.
UPDATE
I changed it to:
=ARRAYFORMULA(if(len(C2:C)=0,,JOIN(" OR Col1 = ", ARRAYFORMULA(if(len(C2:C)=0,,split(C2:C,"+"))))))
So my full formula is:
=ARRAYFORMULA(
if(
len(C2:C)=0,,
join(" / ",
QUERY(
IMPORTRANGE("14iNSavtvjRU0XipPWIMKyHNwXTA85P_CafFTsIPHI6c","RawData!A:AC"),"select Col25 where Col1 = " &
ARRAYFORMULA(
if(len(C2:C)=0,,
JOIN(" OR Col1 = ",
ARRAYFORMULA(
if(
len(C2:C)=0,,split(C2:C,"+")
)
)
)
)
),
0
))))
And now getting the error:
Error
JOIN range must be a single row or a single column.
I believe this formula on the tab called MK.Testing will pull the info you're hoping for.
=QUERY(IMPORTRANGE("14iNSavtvjRU0XipPWIMKyHNwXTA85P_CafFTsIPHI6c","RawData!A:AC"),"select Col25 where Col1="&TEXTJOIN(" or Col1=",TRUE,A2:A))
I think you might have been overcomplicating things? This formula just forms a text string out of the shipment IDs to use in a query. one thing that may be tripping you up is that query() is very particular about the type of data in a column. Your shipment IDs can be numbers, or they can be number letter combos, but not both. That is, if you have some shipment IDs that contain letters and others that don't, it will be more difficult to get a query that works. (though not impossible). For the sake of helping you though, it's important that your sample IDs reflect the real ones in this way as accurately as possible.
How about doing this with Apps Script? You can get the values from the Sheet2, Shipment Ids, and the Ids from MK.Testing and compare them. If they coincide, the you copy the ETA into the Column C of MK. Testing:
function myFunction() {
var sprsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet2 = sprsheet.getSheetByName("Sheet2");
var mkTesting = sprsheet.getSheetByName("MK.Testing");
var shipmentId = sheet2.getRange("A2:A").getValues();
var idList = mkTesting.getRange("A2:A").getValues();
for (var i = 0; i < shipmentId.length; i++){
for (var j = 0; j < idList.length; j++){
if (idList[j][0] == ""){break;} //Stops if there is an empty cell in Mk.Testing's column A
if (idList[j][0] === shipmentId[i][0]){
var eta = sheet2.getRange("E"+(i+2)).getValue();
mkTesting.getRange("C"+(j+2)).setValue(eta);
}
}
}
}
References:
SpreadsheetApp Class
Range Class

Getting the unique values and counts of them in a column of comma separated values in Google Sheets without using JOIN?

I have a column with a bunch of ingredients lists in it. I'm trying to figure out how many times different individual ingredients appear. There are 73,000 rows. The answers on this question works for a small amount of data in Google Sheets.
Formula is =UNIQUE(TRANSPOSE(SPLIT(JOIN(", ";A2:A);", ";FALSE)))
But I've overwhelmed JOIN with more than 50000 characters here. Is there another way to tackle this?
Sheet: https://docs.google.com/spreadsheets/d/1t0P9hMmVpwhI2IbATmIMjobuALTg8VWhl8-AQaq3zIo/edit?usp=sharing
=ARRAYFORMULA(UNIQUE(TRIM(TRANSPOSE(SPLIT(TRANSPOSE(
QUERY(","&A1:A,,5000000)),",")))))
=QUERY(QUERY(ARRAYFORMULA(TRIM(TRANSPOSE(SPLIT(TRANSPOSE(
QUERY(","&A1:A,,5000000)),",")))),
"select Col1,count(Col1)
where Col1 is not null
group by Col1
label count(Col1)''"),
"order by Col2 desc")
demo spreadsheet
=UNIQUE(TRANSPOSE(SPLIT(REGEXREPLACE(TRANSPOSE(
QUERY(ARRAYFORMULA(","&A1:A),,5000000))," ,",","),",")))
but maybe you need this (?):
=QUERY(TRANSPOSE(SPLIT(REGEXREPLACE(TRANSPOSE(
QUERY(ARRAYFORMULA(","&A1:A),,5000000))," ,",","),",")),
"select Col1,count(Col1)
where Col1 is not null
group by Col1
label count(Col1)''")
I did a google scripting solution because I wanted to play with key map pairs.
function myFunction() {
var myMap = {"candy":0};
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getSheetByName("FIRSTSHEETNAME");
var os = sh.getSheetByName("Ingredients");
var data = ss.getDataRange().getValues();
for (var i=0; i<data.length;i++)//full
//for (var i=1; i<4000;i++)//test
{
var array = data[i][0].split( ",");
for (var j=0; j<array.length;j++)
{
var item = array[j];
//Logger.log(array[j]);
if (myMap[item]>-1){
//Logger.log("REPEAT INGREDIENT");
var num = parseInt(myMap[item]);
num++;
myMap[item]=num;
//Logger.log(item +" "+num);
} else {
myMap[item]=1;
//Logger.log("New Ingredient: "+item);
//Logger.log(myMap);
}
}
}
//Logger.log(myMap);
var output=[];
for (var key in myMap){
//Logger.log("Ack");
output.push([key,myMap[key]]);
}
//Logger.log(output);
os.getRange(2,1,output.length,output[0].length).setValues(output);
}
You'll need to add an "Ingredients" tab for the output and change your first tab to be called FIRSTSHEETNAME (or change the code). In my testing it took 4 seconds for 4 items, 5 seconds for 400 items, and 6 seconds for 4000 items. there might be an issue with leading spaces but this gives you a place to start.
A fast running formula that works with columns of at least 40,000 rows:
=query(arrayformula(TRIM(flatten(split(A2:A20000,",")))),"select Col1,Count(Col1) Where NOT (Col1='' OR Col1 contains '#VALUE!') Group By Col1 order by Count(Col1) desc label Col1 'Ingredient',Count(Col1) 'Freq.'")
FLATTEN function, combined with SQL (QUERY function) can be a solution for fast filtering of values (such as empty or error messages).
TRIM function avoids artifacts in the result due to meaningless spaces before/after each string.
Sheet: https://docs.google.com/spreadsheets/d/1m9EvhQB1Leg2H7L52WhPe66_jRrTc8VsnZcliQsxJ7s/edit?usp=sharing
*In case of false case differences, you could normalize all characters of the strings to uppercase before within the same formula with UPPER(A2:A20000).

Countif Indirect Google Sheets

I am trying to write a formula that counts the number of times the number 1 appears in cell F1 of all my sheets. My sheets have varying names (18-0100, 18-0101, 18-0102...). I tried the following formula:
=COUNTIF(INDIRECT("'"&"'!F1"),"=1")
It acts unpredictably. It will only return 1 even if it should be more than 1. And when I try to start trying to count 2 instead of 1 it returns 0 and not the correct number.
What am I doing wrong?
Your formula counts only the current sheet.
To get them all you need to enter all sheet names:
The formula for each sheet is:
=(INDIRECT("'"& sheet_name &"'!F1")=1)*1
You can leverage a Google Apps Script to pull this off as well.
From your spreadsheet, go to Tools > Script Editor. Add the code below, then save. The function saved there will be accessible by all other Google apps, including your spreadsheet. You can then use the name of the function in your spreadsheet like you would with any other formula.
function OccurrencesInSheetNames(str) {
var c = 0;
var regExp = new RegExp('[' + str + ']', 'g');
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var sheetnames = [];
for (var i=0; i<sheets.length; i++) {
sheetnames.push([sheets[i].getName()]);
}
for (var i=0; i<sheetnames.length; i++) {
var name = sheetnames[i].toString();
var count = (name.match(regExp) || []).length;
c += count;
}
return c;
}
Now in your cell F1, place the following formula:
=OccurrencesInSheetNames(TEXT(1,0))
You can also replace the 1 in the above formula with a cell reference, if that works better for your needs. (e.g. =OccurrencesInSheetNames(TEXT(C5,0)) if you want to search through your sheet names for the integer number found in cell C5.)
Here's a demo spreadsheet for you to try out.

Generate string like Q,R,S...AB where AB is last column in Google Sheet

I have a complex Google Sheet query that works great except when a Google Sheet doesn't have as many columns as I use in my formula.
Here's what the formula looks like now:
=sum(filter(query(INDIRECT("'" & A2 & "'!$A$7:$23"),"select Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH where B='"&C2&"'",0),query(INDIRECT("'" & A2 & "'!$A$7:$23"),"select Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH where B='PROJECT'",0) >=date(2017,1,1),query(INDIRECT("'" & A2 & "'!$A$7:$23"),"select Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH where B='PROJECT'",0) <=date(2017,12,31)))
It works great. But the problem is I run it against many worksheets and some don't have e.g. column AG,AH and end at AF at which point I get an error.
So what I need is a way to generate the string Q,R,S....[Name of Last Column in Sheet] and then I can use that instead of my hard-coded Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH but I cannot figure out how to do that.
Any help is greatly appreciated. Thanks!
Per comments above, final formula was:
LEFT("Q,R,S,T,U,V,W,X,Y,Z,AA,AB,AC,AD,AE,AF,AG,AH,AI,AJ,AK,A‌​L,AM,AN,AO,AP,AQ,AR,‌​AS,AT,AU,AV,AW,AX,AY‌​,AZ,BA,BB,BC ",2*Columns(INDIRECT(A2&"!1:1"))-33+IF(Columns(INDIRECT(A2&"‌​!1:1"))>26,Columns(I‌​NDIRECT(A2&"!1:1"))-‌​26,0))
where column A contains the list of worksheets (tabs) in the Google Sheet. Put this in B2, and then copied it down. I am not marking this as the correct answer since others gave a correct formula-based answer but this did the trick for me.
This can be done with built-in functions:
On a helper sheet, let say you name it, helper, fill up range with letters A to Z, let say A1:A26
Let say that on B1 you write the following formula:
=ArrayFormula({A1:A26;TRANSPOSE(SPLIT(JOIN(",",SUBSTITUTE(QUERY(TRANSPOSE(A1:A26)&A1:A26,,27)," ",",")),","))}) . This will create a list of column letter headers.
On each new worksheet use columns(1:1) to get the total number of columns.
To get your string of column headers, then you could use something like :
JOIN(",",OFFSET(helper!B1,16,0,columns(1:1)-16))
QUERY(helper!B:B,"select B limit "&columns(1:1)-7&" offset 7")
NOTE:
If you decide to have only one helper sheet and use it on several spreadsheets, then use
QUERY(IMPORTRANGE(your_url,"helper!B:B"),"select Col1 limit "&columns(1:1)-7&" offset 7")
This can be done with script. Without seeing you spreadsheet, it is hard to know exactly what you need, but this should be close. I get the variables from Sheet1 and return the formula to Sheet1. Adjust the sheet name to fit your needs. This will look at your data sheets based on the variable sheet name determine the last column. Determine the column letters and build the string the query needs. It then sets the new query formula. I added a menu to run it from.
function onOpen() {
SpreadsheetApp.getActiveSpreadsheet().addMenu(
'Create Data', [
{ name: 'Run', functionName: 'formula' },
]);
}
function formula(){
var ss= SpreadsheetApp.getActiveSpreadsheet()
var s=ss.getSheetByName("Sheet1") //sheet where variables are
var sheet=s.getRange("A2").getValue()//variable sheet name
var sel=makeString(sheet) //get the select string of column letters
//Create formula and return to Sheet1 A3
var f= s.getRange("A3").setFormula('=sum(filter(query(INDIRECT("\'" & A2 & "\'!$A$7:$23"),"select '+sel+' where B=\'"&C2&"\'",0),query(INDIRECT("\'" & A2 & "\'!$A$7:$23"),"select '+sel+' where B=\'PROJECT\'",0) >=date(2017,1,1),query(INDIRECT("\'" & A2 & "\'!$A$7:$23"),"select '+sel+' where B=\'PROJECT\'",0) <=date(2017,12,31)))')
}
function makeString(sht){
var ss= SpreadsheetApp.getActiveSpreadsheet()
var s=ss.getSheetByName(sht)
var lc=s.getLastColumn()
var rng=s.getRange(1, 17, 1, lc).getValues()
var str=''
var ltr=[]
for(var i=17;i<rng[0].length+1;i++){
ltr[i]= columnToLetter(i)
str=ltr.join(',')
}
var str1=str.substr(17)
return str1
}
function columnToLetter(column)
{
var temp, letter = '';
while (column > 0)
{
temp = (column - 1) % 26;
letter = String.fromCharCode(temp + 65) + letter;
column = (column - temp - 1) / 26;
}
return letter;
}
Let me know if you have any questions.

Resources