Split Google Sheets Cells into Multiple Rows Based on a Delimiter - javascript

I'm trying to come up with a function that will split data stored in a cell into multiple rows based on a delimiter (in this case, "\n"). I found a really good solution here:
https://webapps.stackexchange.com/questions/60861/google-sheets-split-multi-line-cell-into-new-rows-duplicate-surrounding-row-e
However, this solution only allows you to split up ONE column into multiple rows; I need to split up multiple columns.
Here is an example of some data I have now:
Here is an example of what I would like the final product to look like:
I know I can get this result by using =TRANSPOSE(SPLIT(cell,CHAR(10))) and then copying down the other cells, but I have a lot of data so I'd really prefer to automate this like in the above link. Any help you can give me is appreciated. Thank you!

Restructure rows to accomodate multiple items in a single cell
function restructureRows() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('Sheet2');
const shsr=2;//data start row
const rg=sh.getDataRange();//this includes one header row
const dlm='\n';//cell delimiter
let vs=rg.getValues();//this includes header row
let hA=vs.shift();//this put the header row in hA
var ttl={};//converts array indices to column names
var a=0;//added row counter
hA.forEach(function(h,i){ttl[i]=h;});
//vs nolonger contains header row
for(var i=0;i<vs.length;i++) {
var robj={max:1,maxidx:''};
for(var j=0;j<vs[i].length;j++) {
var temp=vs[i][j].toString();
//if data contain line feed then that row will be expanded
if(vs[i][j].toString().indexOf(dlm)!=-1) {
let t=vs[i][j].toString().split(dlm);//split cell on dlm
robj[ttl[j]]=[];//row expansion object
t.forEach(function(e,k){
robj[ttl[j]].push(e);//push expanded cell data into obj property arrays
});
if(robj[ttl[j]].length>robj.max) {
robj.max=robj[ttl[j]].length;//keeping record of the max number of delimited terms
robj.maxidx=j;//never actually used this yet
}
}else{
robj[ttl[j]]=[];
robj[ttl[j]].push(vs[i][j]);//if no dlm the just same the one term
}
}
if(robj.max>1) {
for(var k=0;k<vs[i].length;k++) {
const l=robj[ttl[k]].length;
if(l>1 && l<robj.max) {
for(let m=l;m<robj.max;m++) {
robj[ttl[k]].push(undefined);//This section fills in the cells that had multiple dlms with undefined so that all columns have the same amount of terms in their array
}
}else if(l==1) {
for(let m=1;m<robj.max;m++) {
robj[ttl[k]].push(vs[i][k]);//this section fills in the cells that have no dlms with the same value for all rows
}
}
}
sh.insertRows(i+shsr+1+a, robj.max-1);//insert addtional row
var oA=[];
for(var r=0;r<robj.max;r++) {
oA[r]=[];
for(var k=0;k<vs[i].length;k++) {
oA[r].push(robj[ttl[k]][r]);//This section loads data into the ouput array in preparation for a single setvalues() to load all new rows at one time.
}
}
sh.getRange(i+shsr+a,1,oA.length,oA[0].length).setValues(oA);//load data
a+=robj.max-1;//increment added row counter
}
}
}
Animation:

Related

How do I input an specific cell for every i in a loop on google script?

One of my projects is making a sales spreadsheet.
The sales spreadsheet contains the names of the products and their prices are in the documentation, the challenge is getting the prices to automatically show up on the cell right next to the product name in the spreadsheet.
Here's what I did:
function Autoprice() {
var sales = SpreadsheetApp.getActive().getSheetByName('Sales')
var salesrow = sales.getRange('D2:D'+sales.getLastRow())
var productnames = salesrow.getValues()
size = productnames.length+1
for (var i = 0; i< size; i++){
if (productnames[i+1]=='Diary')
{
sales.getRange('F'+i).setValue(31.90)
}
And I just input all the prices manually.
The thing is, google script does not read the sales.getRange('F'+1) as I thought it would, and I can't find the correct way to read that for every item in 'DI' cell, i want to put a price on 'FI' cell.
Try using this script, I modified a couple of lines in the sample you shared and added comments next to it to explain.
function Autoprice() {
var sales = SpreadsheetApp.getActive().getSheetByName('Sales')
var salesrow = sales.getRange('D2:D'+sales.getLastRow())
var productnames = salesrow.getValues()
size = productnames.length+1
for (var i = 0; i< size; i++){
if (productnames[i]=='Diary') //If you do productnames[i+1], you're not starting from the beginning of the range, basically you're starting from D3 instead of D2
{
sales.getRange(i+2,6).setValue(31.90) //You can try getRange(row, column) instead
}
}
}
Reference:
getRange(row, column)
You are trying to loop through a 2-dimensionall array (not technically... but each element is a single array).
So to see D2's value you would need productnames[0][0]
However, you can easily fix this using the flat() function. Modify one line of code below:
var productnames = salesrow.getValues().flat();
Also consider learning to use the debugger. If you step through your code, this is easy to see.

Get a value and row number in selected range with gaps between cells?

I need to get values and row number in Google sheet script out of sellected range.
Original problem was that I was trying to get values and row numbers of sellected range using this script:
sheet = ss.getActiveSheet(),
sheetvalues = sheet.getActiveRange().getValues();
for (i=0; i<sheetvalues.length; i++) {
mr = sheet.getActiveRange().getRow()+i;
}
But I found out that if you use filer and select a range then all the hidden cells will also be part of the activerange, but I need only those that are inside of a filtered range. I decided that the best way is to select cells separately one by one.By I can't get a value and row index cause it counts as active range only the last selected cell.
I believe your goal as follows.
Under the selected rows for the sheet, you want to retrieve the row numbers of no selected rows using Google Apps Script.
In this case, I think that the method of getSelection can be used. And, in order to confirm the rows, getActiveRangeList is used. When this is reflected to a sample script, it becomes as follows.
Sample script:
When you use this script, at first, please select the cells like your sample image and run the script.
function myFunction() {
// 1. Retrieve the selected ranges.
const selection = SpreadsheetApp.getSelection();
// 2. Retrieve the selected row numbers.
const selectedRows = selection.getActiveRangeList().getRanges().flatMap(r => {
const row = r.getRow();
const temp = [];
for (let i = 0; i < r.getNumRows(); i++) {
temp.push(row + i);
}
return temp;
}).sort((a, b) => a - b);
// 3. From data range, retrieve the row numbers except for the selected row numbers.
const sheet = selection.getActiveSheet();
const range = sheet.getDataRange();
const rowNumbers = [];
for (let r = 1; r <= range.getNumRows(); r++) {
if (!selectedRows.includes(r)) rowNumbers.push(r);
}
console.log(rowNumbers); // Here, you can see the retrieved row numbers at the log.
// 4. Retrieve the row values of the retrieved row numbers.
// If you want to retrieve the row values of "rowNumbers", you can also the following script.
const allValues = range.getValues();
const values = rowNumbers.map(n => allValues[n]);
console.log(values); // Here, you can see the values of row numbers at the log.
}
As the important point for using the selected range, for example, when the cells of row 3, row 1, row 2 are selected in order, the range list returns the order of cells. So in this sample script, the retrieved row numbers are sorted.
When above script is run, for example, when the data range is "A1:C5" and when the cells "A1", "A3" and "A5" are selected, the row numbers of 2, 4 are retrieved.
References:
getSelection()
getActiveRangeList()

How to combine the getRange() function with a forLoop when trying to piece together your data in Google Apps Script?

EDIT:
Since my problem is rather difficult to describe I added an example of my data which hopefully shows what I'd like to achieve:
https://docs.google.com/spreadsheets/d/1Wa_z2e2br53usul3uMy8nczXushvhPFRiIlZ5M2ueYU/edit?usp=sharing
I hope it's okay to do so.
I could need some help with the following problem: I'm trying to create a summary sheet for a variety of data sheets. Each data sheet, i.e., "Sheet1", "Sheet2", "Sheet3", e.g., has an ID variable that I use for splitting my data within each data sheet.
What I'm trying to do is to loop through all my data sheets, grab the data in each sheet, split the data for each sheet by ID, so that I have all rows with As, Bs and Cs for each sheet, and then put all these pieces together BUT SEPARATED BY A COUPLE OF EMPTY ROWS in my summary sheet.
What I have done thus far is this:
function main() {
// SETUP.
var app = SpreadsheetApp;
var workbook = app.getActiveSpreadsheet();
var activeSheet = workbook.getActiveSheet();
// CREATE NEW SUMMARY SHEET.
var targetSheet = workbook.getSheetByName("Summary");
if (!targetSheet) {
workbook.insertSheet("Summary",1);
}
// ARRAY OF SHEETS USED IN MY LOOP.
var tabs = ["Sheet 1",
"Sheet 2",
"Sheet 3"];
// LOOP FOR ALL SHEETS.
for (var i = 0; i < tabs.length; i++) {
var sheet = workbook.getSheetByName(tabs[i]);
// GRAB THE ORIGINAL DATA.
var originalData = sheet.getRange(5, 1, sheet.getLastRow()-5, sheet.getLastColumn()).getValues();
// SELECT ID AND SORT BY UNIQUE IDs.
var range = sheet.getRange(5,2,sheet.getLastRow()-5,1).getValues();
var range = [].concat.apply([], range);
let uniqueValues = range.filter((v, i, a) => a.indexOf(v) === i);
// GRAB THE UNIQUE DATA PIECES IN EACH SHEET.
for (var t = 0; t < uniqueValues.length; t++) {
var filterText = uniqueValues[t];
var newData = originalData.filter(function(item) {
return item[1] === filterText;
});
// DO SOMETHING I HAVE YET TO DEFINE
// e.g., exclude rows that fall beneath a certain threshold.
// WRITE DATA PIECES BACK TO SUMMARY SHEET.
workbook.getSheetByName("Summary").getRange(???).setValues(newData);
}
}
}
The code above works fine and does slice my data on different data sheets correctly. However, setting the pieces back together is an issue. Right now the data pieces of each iteration is overwritten by the next one.
What I need to do is to figure out a way how to grab these different slices of data based on my ID and then put them together in a way shown in my example data (linke above).
I think I'm lost somewhere between different loops and the data stored temporarily within the loops.
Answer:
After gathering the data you need to:
Run through the rows
Check the product name
Push rows with the same product name to separate arrays
Use setValues() to set the data of each product
Add 4 blank rows
Code Modifications:
Before your LOOP THROUGH ALL SHEETS for loop, add an array declaration:
var dataSets = [];
Then, replace the following line:
workbook.getSheetByName("Summary").getRange(???).setValues(newData);
With:
dataSets.push(newData);
And after the for loop, do the data processing set out in the first section of this answer:
// Create a 2D array to push the data to:
var allData = [[],[],[]];
// loop through the Data sets and push them to the correct element of allData:
dataSets.forEach(function(dataSet) {
dataSet.forEach(function(row) {
if (row[1] == "Product A") {
allData[0].push(row)
}
else if (row[1] == "Product B") {
allData[1].push(row)
}
else if (row[1] == "Product C") {
allData[2].push(row)
}
})
})
// Define the next row to add data to in the Summary sheet:
var nextRow = 1;
// Set the data to the Summary sheet:
allData.forEach(function(product) {
var noOfColumns = product[0].length;
var noOfRows = product.length;
workbook.getSheetByName("Summary").getRange(nextRow, 1, noOfRows, noOfColumns).setValues(product)
nextRow += noOfRows + 4;
});

What is the most efficient way to sort an HTML table via JavaScript?

I'm writing some JS to sort an HTML table: the HTML table has a header row, clicking on the header of a column sorts the rows according to that column. My general algorithm is as follows:
Convert HTML into a JavaScript array.
Sort JavaScript array.
Reconvert JavaScript array to HTML table.
Right now, my code replaces all of the innerHTML elements of the table. Basically, after it sorts the JS array, it touches every node of the table, rewriting it's innerHTML value. I feel like there might be a more efficient way to do this, but I can't think of it.
EDIT:
Here's the code that does part (3) of the algorithm outlined above. I edited out the irrelevant parts (just some string transformations). The indices are a little weird because I skipped the header row when I converted the table to an array, so I'm compensating for that when rewriting the table.
for (var i = 0; i < numRows-1; i++){
for (var j = 0; j < numCols; j++){
var arrCell = tableArr[i][cols[j]];
tableEl.rows[i+1].cells[j].innerHTML = arrCell;
}
}
You could remove the rows from the DOM, do the sort, then add them back onto the DOM.
var tbody = table.tBodies[0];
var placeholder = document.createElement('tbody');
table.replaceChild(placeholder, tbody);
//sort rows in tbody
table.replaceChild(tbody, placeholder);
Use an array to sort the values and a document fragment to perform the update.
function sortRows(tbody, compare, sortDesc) {
//convert html collection to array
var rows = [].slice.call(tbody.rows);
//sort to desired order
rows.sort(compare);
if (sortDesc) {
rows.reverse();
}
//update table
var fragment = document.createDocumentFragment();
rows.forEach(function (row) {
fragment.appendChild(row);
});
tbody.appendChild(fragment);
}
The complexity will be in the compare function. You will need to take into account the column index and any type conversions and caching you want.
This is a basic example that converts the cell text content to an integer.
function sortTable(table, columnIndex) {
while (table && table.tagName !== 'TABLE') {
table = table.parentNode;
}
if (table) {
sortRows(table.tBodies[0], function compare(topRow, bottomRow) {
var topValue = parseInt(topRow.cells[columnIndex].textContent, 10);
var bottomValue = parseInt(bottomRow.cells[columnIndex].textContent, 10);
return (topValue - bottomValue);
});
}
}
function sortRows(tbody, compare, sortDesc) {
//convert html collection to array
var rows = [].slice.call(tbody.rows);
//sort to desired order
rows.sort(compare);
if (sortDesc) {
rows.reverse();
}
//update table
var fragment = document.createDocumentFragment();
rows.forEach(function (row) {
fragment.appendChild(row);
});
tbody.appendChild(fragment);
}
<table>
<thead>
<tr><th onclick="sortTable(this, 0)">Sort</th><th onclick="sortTable(this, 1)">Sort</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>25</td></tr>
<tr><td>3</td><td>12</td></tr>
<tr><td>2</td><td>40</td></tr>
<tr><td>10</td><td>25</td></tr>
</tbody>
</table>
If the data is stored on the server - I would send an Ajax request to the server with the sorting type (ASC, DESC, etc. ), sort the data on the server (using PHP, etc) and then receive the sorted data in JavaScript and write the table completely new.
You can use jQuery and its Datatables plugin.
In your datatable statement, you can add
"bSort": true
and the job will be done by the script. Although if you can find a pure JS way I suggest you use it so you don't add useless weight to your web page.

Javascript: sorting objects

I'm looking for some ideas of how to accomplish this as I am hitting a wall on it.
I have a table that displays data pulled from a MySQL db. The table goes in a sequence of a row of 13 cells with displayed data followed by a hidden row of one cell. The hidden row is toggled by clicking a link in cell index 1 of the previous row. Like so:
row 1 : click this cell to show row 2 : another cell : another cell : ad nauseum till we get to 13 :
row 2 which is hidden
row 3 : click this cell to show row 2 : another cell : another cell : ad nauseum till we get to 13 :
row 4 which is hidden
...
so using jquery I pulled all the rows, then set a test to determine if it was a displayed row or hidden row, if it was displayed then I put that row and the following one into an object and then placed that object into another object, like so:
//this function is for sorting the data in the table
$(".sort").click(function() {
//get what column we are sorting by
var sortBy = $(this).attr('sortBy');
//set the colnum to sort by
if (sortBy == "itemname") {
var sortCol = 1;
} else if (sortBy == "priority") {
var sortCol = 2;
} else if (sortBy == "livedate") {
var sortCol = 10;
} else if (sortBy == "status") {
var sortCol = 11;
} else if (sortBy == "designer") {
var sortCol = 12;
}
//get the table data
var tableData = getTableData("NO", "null", "YES", sortBy);
//get all the rows
var tableRowArray = $("#productTableBody tr");
//declare new table object
var tableObj = new Object;
var rowPackage = new Object;
//loop through tableRowArray and put rows into packages of two, the main row and the hidden row
for(var t=0; t<tableRowArray.length; t++) {
if($(tableRowArray[t]).children(":first").attr('class') == "colorCode") {
rowPackage[t] = $(tableRowArray[t]).children();
rowPackage[t+1] = $(tableRowArray[t+1]).children();
//dump rows into tableObj
tableObj[t] = rowPackage;
}
//clean out rowPackage
rowPackage = {};
}
var x=-2;
for(thisRow in tableObj) {
x = x+2;
var sortItem = $(tableObj[thisRow][x][sortCol]).html();
//ack! -->getting stumped here
}
});
I've also collected which column the user wants to sort by. I can then find the cell the user wants to sort by. I know I need to pull that info, put into an array and sort but I guess I am getting stumped on how to apply the sorted array back to my tableObj so I can rewrite the table body HTML...the reason I am getting stumped is that some of the data to be sorted will be identical, for example if sorting by designer the data could be this {"a", "b", "c", "c", "a", "a", ""a"}, which when sorted would be a, a, a, a, b, c, c, but since some are identical I can't go back and loop through the object and find the entry that matches the first item in my sorted array, 4 items will match it. So how do I determine which entry in the object matches up with the first a in the sorted list?
Thanks in advance.
That's a tough one, but I suppose there is hardly anything impossible in this life.
I would go like this (using Underscore library)
var packs = [];
// assuming you always have even number of tr's
$("#productTableBody tr:odd").each(function(i, tr){
packs.push( {main: tr, sub: tr.next()} );
// tr.next() will be :even, so it's not yet selected
// now detach rows from the table
// note the order - next() wont work otherwise
tr.next().remove();
tr.remove();
});
var sortedPacks = _(packs).sortBy(function(pack){
return $(pack.main).find('td:nth('+sortCol')').text();
});
// now in sortedPacks you have touples of rows sorted by "main" rows sortCol column
// and you would probably want to restore the table now
$.each(packs, function(i, pack){
$("#productTableBody").append(pack.main).append(pack.sub);
});
The code may not reflect your situation perfectly, but I suppose you should be able to get the main idea.
Not really to easy to get what you are asking for here, but this will at least enable you to get the data sorted.
Start by collecting your data into an array, eg data, each row can be represented by either an array or an object.
Now simply call
data.sort(function(a, b){
// select the two values from a and b depending on the column to sort by
a = a[colNr];
b = b[colNr];
return a == b ? 0 : a < b ? -1 : 1;
});
Now you can easily rebuild your table based on the sorted array.
If you during data-collection also added a reference to the row to the array/object, then you can now remove all rows from the table, loop over the data array, and add each node back to the table.

Categories