Unpivot in Google Sheets - multipe header columns

2020-07-25 09:47发布

问题:

I have a summary table with about 20 columns and up to a hundred of rows, however I would like to convert it into a flat list so I can import to a database.

This solution is not working properly in my case and my knowledge of JS is far beneath the ability to adjust it properly.

There're three tabs in the Example sheet:

  1. Source Data - dummy data of what I currently have
  2. Desired Result - what I want to convert Source Data to
  3. What I Get - result I get when using the solution mentioned above

Sheet is shared, so you can try and test the script (Menu > Script > Run). It would create a new tab automatically.

回答1:

Here's what I came up with after some JS learning and googling. If anyone can suggest how to make it shorter/cleaner/simpler - I'm all ears. Might be far from perfect, but it does exactly what I need.

function transpose(){
  var source = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('SOURCE DATA');
  var ss = SpreadsheetApp.getActiveSpreadsheet();

  var numColumns = source.getLastColumn();
  var numRows = source.getLastRow();
  Logger.log('numColumns = ' +numColumns);
  Logger.log('numRows = ' +numRows);


  //GET NUMBER OF HEADERS (PRODUCTS)

  var products = []; // get product models in the first row
  for (var b = 2; b <= numColumns; b++){
    if (source.getRange(1, b).getValue() != "") {
      products.push([source.getRange(1, b).getValue()]); //store
    }
  }


  // PRODUCTS and SITES INTO COLUMNS

  var output = [];

  var sites = []; // get sites list
  for (var a = 3; a <= numRows; a++){
    if (source.getRange(a, 1).getValue() != "") {
      sites.push([source.getRange(a, 1).getValue()]); //store
    }
  }

  for(var p in products){
    for(var s in sites){
      var row = [];
      row.push(sites[s]);
      row.push(products[p]);
      output.push(row);//collect data in separate rows in output array
    }
   }

  var date = Utilities.formatDate(new Date(), SpreadsheetApp.getActive().getSpreadsheetTimeZone(), "M/d/yyyy");
  Logger.log('Date = ' +date)
  ss.insertSheet(date,0).getRange(1,1,output.length,output[0].length).setValues(output);

    var newSheet = ss.getSheetByName(date);


  // COPY REGIONS

  var numProducts = products.length; // number of models
  Logger.log('numProducts = ' +numProducts);

  var i = 1;
  var j = 3 // first column number to copy
  do {
    var colC = newSheet.getRange("C1:C").getValues();
    var copyToCell = colC.filter(String).length+1;
    Logger.log('copyTo R = ' +copyToCell);

    source.getRange(3,2,numRows-2,1).copyTo(newSheet.getRange(copyToCell,3), {contentsOnly:true});
    i++;

    source.getRange(3,j,numRows-2,2).copyTo(newSheet.getRange(copyToCell,4), {contentsOnly:true});
    j+=2;  
  }
  while (i <= numProducts);
  while (j < numColumns);


  // SORT BY SITE AND PRODUCT

  newSheet.getDataRange().sort([1, 2]);
}


回答2:

I wrote this easy general custom function for unpivoting / reverse-pivoting in Google Spreadsheet. Note: this is a custom function like any other built-in spreadsheet function, so you don't have to run it, you can use it like unique() or any other array function.

In your case it would as easy as using the following code: =unpivot(A1:H6,2,2,"product","MIN","MAX")

You can find the example here: https://docs.google.com/spreadsheets/d/12TBoX2UI_Yu2MA2ZN3p9f-cZsySE4et1slwpgjZbSzw/edit#gid=422214765

And this is the code:

/**
 * 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 && data[i][0];i++)
  {
    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;
}


回答3:

I think you can use CONCAT to get a unique row key from location and region and use the method from my answer here which doesn't require extra scripts.

If you do something fancy with the key concatenation (e.g. CONCAT(A3, "~", A4)), you can even extract your original location and region in the unpivot table (e.g. REGEXEXTRACT(A1, "(\w+)~") and REGEXEXTRACT(A1, "~(\w+)")).