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
Related
CONTEXT
I have a source table with multiple columns (Source B table in example spreadsheet).
On column E we have max one name and on column F we can have from 0 to multiple names separated by a comma.
AIM
When there are values in F, add one row per each name (E,F - the last one can have more than one separated by a comma) and duplicate the common values.
It should keep the rows where there are no values in F.
The final result will have one less column than Source table.
WHAT I'VE TRIED
=ARRAYFORMULA(QUERY(IFERROR(SPLIT(FLATTEN(IF(ISBLANK(E3:F6);;
A3:A6&"♦"&B3:B6&"♦"&C3:C6&"♦"&D3:E6&"♦"&E3:E6&"♦"&G3:G6&"♦"&H3:H6&"♦"&I3:I6&"♦"&J3:J6&"♦"&K3:K6&"♦"&L3:L6)); "♦"));
"where Col3 <> 0"; 0))
Problem
This formula was applied to Source A table (in the example spreadsheet and that's = Source B table but without comma separated values in F), which didn't have more than one value in F at the time, and:
duplicates the common values as expected but it's not showing the values from F. Just duplicates the ones from E.
ads blank row if the source row doesn't have values in F
because in column K only one row has a value, it messes up the final data
doesn't do anything different with or without comma separated values in F
Example spreadsheet
--- EDIT ---
I've found this other post with a script that I also tested on the example sheet. I've reduced the number of columns to ease the test and because I think it's aiming for the last column being the one that is supposed to be splitted, if applicable.
It does split but it repeats the "Main" client on the left column and the splited one on the right, where I would like the outcome to have all clients on the same column.
To give you an idea, you can try the following script, I used the "Source for Script" Sheet and created another sheet called "Result". As you can see in the script, I used arrays to collect the data, then added it to the "Result" Sheet.
function splitNewRow() {
var source = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Source for Script");
var data = source.getRange(2, 1, source.getLastRow(), source.getLastColumn()).getValues();
var outcomeSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Result");
newArray = [];
newRow = [];
for (i=0; i < data.length; i++){
var claim = source.getRange(i+2,1).getValue();
var mainClient = source.getRange(i+2,2).getValue();
var others = source.getRange(i+2, 3).getValue();
hasComma = others.indexOf(",") != -1;
if (others != "" && hasComma == true){
newArray = [];
newArray = others.split(",");
newArray.push(mainClient);
for (j=0; j < newArray.length; j++){
newRow = [claim, newArray[j]];
outcomeSheet.appendRow(newRow);
}
}
else if(others != "" && hasComma == false){
newRow = [claim, mainClient];
outcomeSheet.appendRow(newRow);
newRow = [claim, others];
outcomeSheet.appendRow(newRow);
}
else{
newRow = [claim, mainClient];
outcomeSheet.appendRow(newRow);
}
}
}
After running the script, you get the following:
If you have any questions, let me know.
Say we have the following spreadsheet in google sheets:
a a
b b
c
d e
e d
How would I build a formula that counts the number of rows in column B that do not match the corresponding row in column A, and are not blank? In other words I want to get the number of rows that changed to a new letter in column B. So in this example the formula should return 2.
Thank you for your help.
UPDATE:
Now suppose I have this spreadsheet:
a a
b b b
c a
d e e
e d e
How would I build on the last formula for the third column, where the value returned is:
(the number of rows in column 3 that don't match the corresponding row in column 2) + (if column 2 is blank, the number of rows in column 3 that do not match the corresponding row in column 1)
and I also don't want to count blanks in the third column.
The value returned in this case should be 2 (rows 3 and 5).
To me it sounds like you could use:
=SUMPRODUCT((B:B<>"")*(B:B<>A:A))
=IFNA(ROWS(FILTER(A:B,
(A:A<>B:B)*
(B:B<>"")
)),0)
FILTER by matching conditions * for AND + for OR.
ROWS counts rows
IFNA returns 0 if nothing was found.
or with QUERY
=INDEX(QUERY(A:B,"select count(B) where B<>A"),2)
Try this:
=ARRAYFORMULA(COUNTA($B$1:$B)-SUM(COUNTIFS($A$1:$A, $B$1:$B,$B$1:$B,"<>")))
I see 2 ways to complete this.
First you could add a function to each row to return 1 or 0 if the value changed and was not blank and then sum results. This unfortunately adds a messy column in your spreadsheet.
=if(A1<>IF(ISBLANK(B1),A1,B1),1,0)
Second you could create a function where you would pass the range as a string.
The call from the worksheet would look like this:
=myFunction("A1:B5")
Then create a script by opening Tools -> Script editor and use something like this
function myFunction(r) {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange(r);
var numRows = range.getNumRows();
var areDifferent = 0;
for (let i=1; i<= numRows; i++) {
let currentValue = range.getCell(i,1).getValue();
let cmpValue = range.getCell(i,2).getValue();
if ((currentValue != cmpValue) && (cmpValue != "")) {
areDifferent++;
}
}
return areDifferent;
}
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
There were a lot of questions with transpose but I have some specifics and can't create the right formula.
So I have near 100k rows in the following format
https://docs.google.com/spreadsheets/d/146-6YHv69DDOnPKheKeRFZIQeZYIgO6UwCMd7X9VeKU/edit?usp=sharing
I need ARRAYFORMULA or something like that to make this 100k rows in the format that you can see on the "Expected Outcome" tab.
=ARRAYFORMULA({UNIQUE(INDIRECT("source!A2:A"&COUNTA(source!A2:A))),
QUERY(SPLIT(TRANSPOSE(SPLIT(QUERY("♦"&INDEX(SPLIT(TRANSPOSE(SPLIT(
TRIM(QUERY(TRANSPOSE(QUERY(TRANSPOSE(
IF(source!B2:J<>"", "♦"&source!A2:A&"♠"&{"♥"&source!B2:B, source!C2:J}, ))
,,999^99)),,999^99)), "♦")), "♠"),,2),,999^99), "♥")), "♦"),
"where Col2 is not null", 0)})
You have a vast number of records but there are 19 data fields to each record spread over 10 columns & 12 rows. You suggested an arrayformula (and much more clever people than I can probably do that), but I suggest a script which takes the data in its current form on one sheet (say the 'source'), and which outputs the data to a new sheet (say, the 'target').
There are a couple of issues to be addressed in "mapping" the data:
how many records are represented on the input sheet - required to enable looping. I used the Javascript Math.floor method to calculate the number of products.
identify the correct row/column combination for each field. The data is in three segments
8 fields in contiguous columns in the first row,
2 fields in 2 rows in the ninth column, and
9 fields in nine contiguous rows in the tenth column.
Total = 12 rows/10 columns. To do this, in order to navigate the rows, I took the counter (i) multiplied by the number of rows, plus 1; the columns are more intuitive.
for efficiency,
get data once only at the beginning of the script;
use arrays to progressively build the output; and
update the output (setValues) once at the end of the script.
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourcesheetname = "source";
var targetsheetname = "target";
var source = ss.getSheetByName(sourcesheetname);
var target = ss.getSheetByName(targetsheetname);
var sourcerange = source.getDataRange();
var sourcedata = sourcerange.getValues();
var sourceheaders = 1;
var rowsperproduct = 12;
var sourcelr = source.getLastRow();
var integerPart = Math.floor((sourcelr-sourceheaders)/rowsperproduct);
//Logger.log("DEBUG: number of products = "+integerPart);
var rowdata = [];
// look thought the number of products
for (var i = 0; i<integerPart; i++){
// use a temporary array to build the data for the row.
var temp = [];
// row 1, copy first eight columns (0-7)
temp.push(sourcedata[(i*12)+1][0]);//ID
temp.push(sourcedata[(i*12)+1][1]);//GID
temp.push(sourcedata[(i*12)+1][2]);//NAME
temp.push(sourcedata[(i*12)+1][3]);//PRICE
temp.push(sourcedata[(i*12)+1][4]);//BRAND
temp.push(sourcedata[(i*12)+1][5]);//URL
temp.push(sourcedata[(i*12)+1][6]);//country
temp.push(sourcedata[(i*12)+1][7]);//instock
// row 2 & 3 in Column I(8)
temp.push(sourcedata[(i*12)+2][8]);//url1
temp.push(sourcedata[(i*12)+3][8]);//url2
// rows 4 - 12 in Column J(9)
temp.push(sourcedata[(i*12)+4][9]);// tech name
temp.push(sourcedata[(i*12)+5][9]);// size
temp.push(sourcedata[(i*12)+6][9]);// inches
temp.push(sourcedata[(i*12)+7][9]);// mm
temp.push(sourcedata[(i*12)+8][9]);// depth
temp.push(sourcedata[(i*12)+9][9]);// BB
temp.push(sourcedata[(i*12)+10][9]);// ref1
temp.push(sourcedata[(i*12)+11][9]);// ref2
temp.push(sourcedata[(i*12)+12][9]);// ref3
// Update the rowdata for this row
rowdata.push(temp);
}
// update data to target
target.getRange(2,1,integerPart,19).setValues(rowdata);
}
First of all I must say that my level of knowledge of Google App Scripts is almost nil. I tried to create a script using the macro utility, but the resulting code is too long. I will be very grateful if someone can help me simplify this code. I indicate below the parameters of the subject:
I have approximately 50 tabs (sheets) in a spreadsheet, although they can be more. (For short, the spreadsheet that is attached as an example has 5 tabs, whose content, also for short, is the same.)
Each tab contains data in cells B8:L17 (11 columns and 10 rows of data).
On a tab called "Index" are the names of the 50 tabs. The names of the tabs must begin in B2. Between one tab name and the next there are 9 empty rows. (All the above I have been able to solve.)
What I want to do --using the "Indirect" formula- is the following:
- That the data of sheet 1 be copied in Index in C2: M11.
- That the data on sheet 2 be copied to Index at C12: M21.
- That the data on sheet 3 be copied to Index in C22: M31.
- That the data of sheet 4 be copied in Index in C32: M41.
- That the data of sheet 5 be copied in Index in C42: M51.
--Etc.
Thank you for your attention.
function INDIRECT() {
var s = SpreadsheetApp.getActive();
s.getRange("c2").setFormula('=INDIRECT("\'"&Index!b2&"\'!b8:l17")');
s.getRange("c2").offset(10, 0).activate();
s.getCurrentCell().offset(-10, 0).copyTo(s.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_NORMAL, false);
s.getCurrentCell().offset(10, 0).activate();
s.getCurrentCell().offset(-20, 0).copyTo(s.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_NORMAL, false);
s.getCurrentCell().offset(10, 0).activate();
s.getCurrentCell().offset(-30, 0).copyTo(s.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_NORMAL, false);
s.getCurrentCell().offset(10, 0).activate();
s.getCurrentCell().offset(-40, 0).copyTo(s.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_NORMAL, false);
}
The OP needs to populate a master sheet with the contents of other sheets.
The following code assumes that the names of the sheets are already populated in Column B (as per the OP spreadsheet).
The equivalent query formula (in Cell C3, for example) looks like this:
=query(Funda!$B$8:$L$17;" Select * ").
This code does not produce a custom formula. It is a routine that might be run only once, looping through Column B of the Master sheet, and inserting the appropriate query into Column C, based on the sheet names listed in Column B. The code might be run only once. Unless there is a change in the spreadsheet layout, or more sheets are added, the query formulas inserted by the code will update the master sheet for all changes on sub-sheets.
function so54463600() {
// setup spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var mastername = "Master"; // Change this sheet name to whatever you want
var sheet = ss.getSheetByName(mastername);
// The sheet data range
var datarange = "$B$8:$L$17";
// set the data and number of sheets in Column B
var Bvals = sheet.getRange("B1:B").getValues();
var Blast = Bvals.filter(String).length;
var lastRow = sheet.getLastRow();
//Logger.log("DEBUG: the last row = "+lastRow);//DEBUG
// loop through the rows of column B
for (var x = 0; x < lastRow; x++) {
// action if the cell has contents
if (Bvals[x][0].length != 0) {
// Logger.log("DEBUG: x = "+x+", Value = "+Bvals[x][0]+", length: "+Bvals[x][0].length);//DEBUG
// sample query: =query(Funda!$B$8:$L$17;" Select * ")
var formula = "=query(" + Bvals[x][0] + "!" + datarange + ";" + '"' + " Select * " + '")';
// Logger.log("DEBUG: the formula is "+formula);//DEBUG
// Location to set query formula
Logger.log("target range:" + sheet.getRange(x + 1, 3).getA1Notation());
// set the formula
sheet.getRange(x + 1, 3).setFormula(formula);
}
}
}
REVISION 4 Feb 2019
Update Sheet Names Programmatically
The number of sheets in a spreadsheet can be 50 or more. Sheet names can change, sheets can be added or deleted. Therefore, in order to ensure that the data shown on the master sheet is complete and accurate, it is desirable that sheet names be inserted programmatically, rather than manually.
This revision includes a new subroutine buildsheetnames AND some changes to the original code. Regard this is a complete substitution rather than a simple add-on.
Sourcing sheet names
The sheet names are obtained first with getSheets(), and then getName().
The user organises the sheets in the screen order that suits them; only sheets listed to the right of the "Master" sheet will be included in the names displayed by the routine.
A user defined variable startingsheetNumber is included to define the first sheet to be displayed on the "Master" list. This is obtained by counting ALL the sheets from left to right, beginning the count at zero. The count value of the first sheet to the right of "Master" should be assigned to this variable. The rest of the sheets to the right of "Master" will be automatically included.
function so54463600mk2() {
// setup spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var mastername = "Master";
var sheet = ss.getSheetByName(mastername);
// The sheet data range
var datarange = "$B$8:$L$17";
// select the existing sheet data and clear the contents
// do this in case a sheet is added or deleted; minimize chance of not detecting an error.
var lastRow = sheet.getLastRow();
var lastColumn = sheet.getLastColumn();
//Logger.log("last row = "+lastRow+", last Column: "+lastColumn);
var masterrange = sheet.getRange(2, 2, lastRow - 1, lastColumn - 1);
masterrange.clear();
// update the sheet names in column B
// run the sub-routine buildsheetnames
buildsheetnames(mastername);
// set the data and number of sheets in Column B
var Bvals = sheet.getRange("B1:B").getValues();
var Blast = Bvals.filter(String).length;
var lastRow = sheet.getLastRow();
//Logger.log("DEBUG: the last row = "+lastRow);//DEBUG
// loop through the rows of column B
for (var x = 0; x < lastRow; x++) {
// action if the cell has contents
if (Bvals[x][0].length != 0) {
// Logger.log("DEBUG: x = "+x+", Value = "+Bvals[x][0]+", length: "+Bvals[x][0].length);//DEBUG
// sample query: =query(Funda!$B$8:$L$17;" Select * ")
//var formula = "=query(" + Bvals[x][0] + "!"+datarange+";"+'"'+" Select * "+'")';
var formula = "=query(" + "'" + Bvals[x][0] + "'!" + datarange + ";" + '"' + " Select * " + '")';
// Logger.log("DEBUG: the formula is "+formula);//DEBUG
// Location to set query formula
Logger.log("target range:" + sheet.getRange(x + 1, 3).getA1Notation());
// set the formula
sheet.getRange(x + 1, 3).setFormula(formula);
}
}
}
function buildsheetnames(mastername) {
// setup spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(mastername);
// move the curcor to cell B2
var row = 2;
var column = 2;
sheet.getRange(2, 2).activate();
// get the list of sheets
var sheetlist = ss.getSheets();
// Logger.log("DEBUG: Number of sheets: "+sheetlist.length);//DEBUG
// Displaying Sheet names on the master sheet
// Instructions:
// 1 - View the spreadsheet with sheets laid out at the bottom of the screen
// 2 - Move the sheets into the order that you want;
// 2a - Sheets to the left of "Master" (or whatever sheet you define as "mastername") wouldn't be included in the sheet names written onto Master
// 2b - Only the names of those sheets to the right of "Master" will be displayed.
// 3 - Count ALL the sheets, from left to right of screen. The first sheet will be zero, the second sheet will be one, and so on.
// 4 - Stop when you reach the first sheet that you want the name displayed on Master
// 5 - Update the count value for that sheet to the variable 'startingsheetNumber'.
// 6 - Note: the variable "sheetrowseparation" is the number of rows between each name;
// 6a - This is the same as the number of data rows on each data sheet.
// 6b - If the number of data rows ever changes, then you can change this variable.
var startingsheetNumber = 6;
var sheetrowseparation = 10;
// loop through the sheets
if (sheetlist.length > 1) {
for (var i = startingsheetNumber; i < sheetlist.length; i++) {
//Logger.log("DEBUG: Sheet#: "+i+", Sheet name: "+sheetlist[i].getName());//DEBUG
sheet.getRange(row, column).setValue(sheetlist[i].getName());
row = row + sheetrowseparation;
}
}
return;
}