Google Appscript transpose dynamic data group from one column - javascript

I've been jogging my brain trying to figure out how to write this script to transpose data from one sheet to another from a pretty dirty sheet.
There are other questions like this but none seem to be like my particular use case.
This is how the sheet is currently structured (somewhat):
The biggest issue here is I have no concrete idea how many rows a particular group of data will be, But I know there are always a bunch of blank rows between each group of data.
I found a script that took me half way:
function myFunction() {
//Get values of all nonEmpty cells
var ss = SpreadsheetApp.getActiveSheet();
var values = ss.getRange("D:D").getValues().filter(String);
//Create object with 3 columns max
var pasteValues = [];
var row = ["","",""];
for (i = 1; i<values.length+1; i++){
row.splice((i%3)-1,1,values[i-1]);
if(i%3 == 0){
pasteValues.push(row);
var row = ["","",""]
}
}
if(row != []){
pasteValues.push(row)
}
//Paste the object in columns A to C
ss.getRange(1,1,pasteValues.length,pasteValues[0].length).setValues(pasteValues);
}
But in that case the asker dataset was fixed. I can loosely say that the max number of rows each group would have is 10(this is an assumption after browsing 3000 rows of the sheet...but if the script can know this automatically then it would be more dynamic). So with that in mind...and after butchering the script...I came up with this...which in no way works how it should currently(not all the data is being copied):
function myFunction() {
var copyfrom = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('copyfrom')
var copyto = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('copyto')
var values = copyfrom.getRange("A:A").getValues().filter(Array);
var pasteValues = [];
var row = [];
for (i = 1; i<values.length; i++){
if(values[i] != ""){
row.push(values[i])
}
Logger.log(row);
if(i%10 == 0){
pasteValues.push(row);
row = []
}
}
if(row != []){
pasteValues.push(row)
}
copyto.getRange(1,1,pasteValues.length,pasteValues[0].length).setValues(pasteValues);
}
I'm pretty sure I should maybe still be using array.splice() but haven't been successful trying to implement it achieve what i want, here's how the transposed sheet should look:
Info:
Each group of addresses inside the "copyfrom" sheet would be separated by at least 1 blank line
The length of an address group is not static, some can have 5 rows, others can have 8, but address groups are always separated by blank rows
Any help is appreciated

You are right to iterate all input values, and I can suggest the similar code:
function myFunction() {
var copyfrom = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('copyfrom')
var copyto = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('copyto')
var values = copyfrom.getRange("A:A").getValues();
var pasteValues = [[]]; // keep result data here
values.forEach(function(v) { // Iterate all input values
// The last row to be filled in currently
var row = pasteValues[pasteValues.length - 1];
if (v[0]) {
row.push(v[0]);
} else if (row.length > 0) {
while (row.length < 10) {
row.push(''); // Adjust row length
}
pasteValues.push([]);
}
});
if (pasteValues[pasteValues.length - 1].length == 0) pasteValues.pop();
copyto.getRange(1, 1, pasteValues.length, pasteValues[0].length).setValues(pasteValues);
}

Solution:
Assuming that every new row begins with Name, you can use this script to rearrange the column:
function myFunction() {
var copyfrom = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('copyFrom');
var copyto = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('copyTo');
var lastRow = copyfrom.getLastRow();
var values = copyfrom.getRange(1,1,lastRow).getValues().filter(Array);
var pasteValues = [];
var row = [];
var maxLen = 1;
// rearrange rows
for (i = 0; i < values.length; i++) {
if (values[i] == "Name" && i > 0) {
pasteValues.push(row);
row = [values[i]];
}
else if (values[i] != "") {
row.push(values[i]);
if (row.length > maxLen) {
maxLen = row.length;
}
}
}
pasteValues.push(row);
// append spaces to make the row lengths the same
for (j = 0; j < pasteValues.length; j++) {
while (pasteValues[j].length < maxLen) {
pasteValues[j].push('');
}
}
copyto.getRange(1,1,pasteValues.length,maxLen).setValues(pasteValues);
}
Sample I/O:

As far as I can tell, there is no way to get the columns to line up in the output since you don't have any way to tell the difference between, for example, an "address 2" and a "City".
However, as far as merely grouping and transposing each address. This one formula, in one cell, in the tab here called MK.Help will work from the data you provided. It will work for as many contacts as you have.
=ARRAYFORMULA(QUERY(QUERY({A:A,IFERROR(LOOKUP(ROW(A:A),FILTER(ROW(A:A),A:A="")),0),COUNTIFS(IFERROR(LOOKUP(ROW(A:A),FILTER(ROW(A:A),A:A="")),0),IFERROR(LOOKUP(ROW(A:A),FILTER(ROW(A:A),A:A="")),0),A:A,"<>",ROW(A:A),"<="&ROW(A:A))},"select MAX(Col1) where Col1<>'' group by Col2 pivot Col3",0),"offset 1",0))

Related

Remove duplicates across multiple sheets

I want to remove duplicates across 2 different sheets.
I have my active sheet, and I want to remove duplicates that already exist in my sheet "Blacklist". I want to run this process for both Column A and Column B (or simply for any values across the entire sheets). When a duplicate is found, I want to leave the row in tact but replace the value with '' (e.g. an empty cell).
I have a working version I mangled together, but only for the active sheet.
N.B. it's the findDuplicate function that I use, the removeDuplicate function I left there not to mess anything up :)
// this is a Google Apps Script project
function onOpen() {
var spreadsheet = SpreadsheetApp.getActive();
var menuItems = [
{ name: 'Find duplicates...', functionName: 'findDuplicate' },
{ name: 'Remove duplicates...', functionName: 'removeDuplicate' }
];
spreadsheet.addMenu('Duplicates', menuItems);
}
function removeDuplicate() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getActiveRange();
var data = range.getValues();
var rowNum = range.getRow();
var columnNum = range.getColumn();
var columnLength = data[0].length;
var uniqueData = [];
var duplicateData = [];
// iterate through each 'row' of the selected range
// x is
// y is
var x = 0;
var y = data.length;
// when row is
while (x < y) {
var row = data[x];
var duplicate = false;
// iterate through the uniqueData array to see if 'row' already exists
for (var j = 0; j < uniqueData.length; j++) {
if (row.join() == uniqueData[j].join()) {
// if there is a duplicate, delete the 'row' from the sheet and add it to the duplicateData array
duplicate = true;
var duplicateRange = sheet.getRange(
rowNum + x,
columnNum,
1,
columnLength
);
duplicateRange.deleteCells(SpreadsheetApp.Dimension.ROWS);
duplicateData.push(row);
// rows shift up by one when duplicate is deleted
// in effect, it skips a line
// so we need to decrement x to stay in the same line
x--;
y--;
range = sheet.getActiveRange();
data = range.getValues();
// return;
}
}
// if there are no duplicates, add 'row' to the uniqueData array
if (!duplicate) {
uniqueData.push(row);
}
x++;
}
}
function findDuplicate() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getActiveRange();
var data = range.getValues();
var rowNum = range.getRow();
var columnNum = range.getColumn();
var columnLength = data[0].length;
var uniqueData = [];
// iterate through each 'row' of the selected range
for (var i = 0; i < data.length; i++) {
var row = data[i];
var duplicate = false;
// iterate through the uniqueData array to see if 'row' already exists
for (var j = 0; j < uniqueData.length; j++) {
if (row.join() == uniqueData[j].join()) {
// if there is a duplicate, highlight the 'row' from the sheet
duplicate = true;
var duplicateRange = sheet.getRange(
rowNum + i,
columnNum,
1,
columnLength
);
duplicateRange.setValue('');
}
}
// if there are no duplicates, add 'row' to the uniqueData array
if (!duplicate) {
uniqueData.push(row);
}
}
}
Thanks so much for your help! I've been at this for a few hours and figured I should just ask the experts for advice :)
The first lines of both your removeDuplicate and findDuplicate function seems indeed to indicate that you refer to the active spreadsheet / sheet / range
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getActiveRange();
var data = range.getValues();
If you want to be able to use the same function for a given spreadsheet / sheet / range which is not the active one, you will need to use other functions than the getActiveXXX().
For example, to get the sheet named "Blacklist", you should use
sheet = spreadsheet.getSheetByName("Blacklist")
(see also https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet#getsheetbynamename)
If you want to access a specific range which differs from the active range, you should use the getRange method (see also https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet#getrangea1notation)
Note that getRange method can be used in different ways, e.g.
getRange("A1:D4"), getRange(1, 1, 3, 3) (the parameters being respectively startRow, startColumn, numRows,numColumns)
Additionally, if you don't want to hardcode the last line of your 2 columns, you will most probably need this function to find the last line in the code :
https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet#getlastrow
(there is also an example there showing how to use getRange() in combination with getLastRow()).
I hope this will help you going further.
Please note that I didn't check the rest of your code and just assumed that your deduplication logic works fine as you mentioned it in your commment.
Good luck !

How to create priorities list and change them without leaving any gaps

I'm trying to write a simple script for google spreadsheet.
The code should work as follows: it should be a priority list, which can be simply edited. For example I have a list of 7 tasks and want to change the priority of one task. My code simply looks for values greater or eqauls then the value I put in a cell and increments them by 1. So at the end the last task has priority 8 and I have only 7 tasks, so there is one gap somewhere.
Here is what I've already done:
function onEdit(e) {
var range = e.range;
var numRowsWithData = SpreadsheetApp.getActiveSheet().getDataRange().getNumRows();
var columnOfCellEdited = range.getColumn();
var cellEdited = range.getA1Notation();
if (columnOfCellEdited === 2) {
var valueOfEditedCell = range.getValue();
for (var i = 2; i <=numRowsWithData; i++) {
var rangeSheet = SpreadsheetApp.getActiveSheet().getDataRange();
var currentCell = rangeSheet.getCell(i, 2);
var currentValue = currentCell.getValue();
if(currentValue >= valueOfEditedCell) {
if(cellEdited == currentCell.getA1Notation()){
continue;
}
currentCell.setValue(currentValue+1);
};
}
};
};
This script works, but I've got some missing numbers, because every time I add 1 to the current value.
How to edit it to have all the numbers in proper order, without any missing ones?
I know it's not very complicated, but I don't have any idea for now:/
Thanks in advance!
Completely rewritten answer, following clarification of the question.
This function reads all of the existing priorities, including the new entry. Any existing value greater than the new one is incremented by 1. An array is used to store the row on which each value occurs. The array is then looped over, skipping any undefined entries (missing numbers), and renumbering those that do exist starting from 1.
This doesn't change the order of the rows. It simply renumbers the second column.
Where possible I've left your original code intact, and re-used your approach in the new bit.
function onEdit(e) {
var range = e.range;
var numRowsWithData = SpreadsheetApp.getActiveSheet().getDataRange().getNumRows();
var columnOfCellEdited = range.getColumn();
var cellEdited = range.getA1Notation();
if (columnOfCellEdited === 2) {
var valueOfEditedCell = range.getValue();
// get snapshot of current priorities
var priorityRows = [];
for (var row = 2; row <=numRowsWithData; row++) {
var rangeSheet = SpreadsheetApp.getActiveSheet().getDataRange();
var currentCell = rangeSheet.getCell(row, 2);
var currentValue = currentCell.getValue();
if(currentValue >= valueOfEditedCell && cellEdited !== currentCell.getA1Notation()) {
currentValue++;
};
priorityRows[currentValue] = row;
}
// renumber priorities
var newPriority = 1;
for (var i = 0; i < priorityRows.length; i++) {
var row = priorityRows[i];
if (row) {
var rangeSheet = SpreadsheetApp.getActiveSheet().getDataRange();
var currentCell = rangeSheet.getCell(row, 2);
currentCell.setValue(newPriority++);
}
}
}
}

Google Apps Script - .setValue in cell based on for loop matching

I'm attempting to grab the values from a data range, loop over the data, match a value in that data, then based on a matching value, update cell located a few columns over.
I'm able to locate value to match, but I'm having a hard time understanding how to update the cell a few columns over. Below is the code I've gotten so far minus the .setValue piece.
var trackingSS = 'Spreadsheet 1';
var decisionSS = 'Spreadsheet 2';
function grabRequestID() {
var ss = SpreadsheetApp.openById(decissionSS);
var range = ss.getActiveSheet().getRange(ss.getLastRow(), 2, 1, 1)
var requestID = range.getValue();
return requestID;
}
function managersDecision() {
var ss = SpreadsheetApp.openById(trackingSS)
var sheet = ss.getSheetByName('Requests');
var data = sheet.getDataRange().getValues();
var requestID = grabRequestID();
for (var i=0; i < data.length; i++) {
for (var j=0; j < data[i].length; j++) {
if (data[i][j] == requestID) {
Logger.log('found it');
}
}
}
}
As you can see there are two functions. managersDecision() reads in all the data from spreadsheet 1. It then calls grabRequestID() and uses this value (from spreadsheet 2) as the matching criteria as it loops over the data from spreadsheet 1. Currently it will locate and find the match.
What I want to have happen now, is based on the match, go over two columns in the same row and update the cell value to "approved" or "denied" based on successfully finding a match.
I'm a bit lost on how to get it to write to the cell. Should I try and identify the row its in and then work to set the value? Maybe grab the entire row the match is in (into an array) and then rewrite the row?
Any assistance would be appreciated..
To set a value you just need to take in count that you are working with an array that start at zero, but in the spreadsheet we start counting at one. You'll also need to be sure that you are trying to write in an existing cell. So prior writing, add the necessary column.
I didn't wrote the "denied" part as it was going through all the cell of the spreadsheet, but I wrote a second version of the managersDecision function where I only go through one column and here I took care of that denied part.
here the code:
var trackingSS = 'Spreadsheet1';
var decisionSS = 'Spreadsheet2';
function grabRequestID() {
var ss = SpreadsheetApp.openById(decisionSS);
var range = ss.getActiveSheet().getRange(ss.getLastRow(), 2, 1, 1)
var requestID = range.getValue();
Logger.log("requestID= "+requestID);
return requestID;
}
function managersDecision() {
var ss = SpreadsheetApp.openById(trackingSS)
var sheet = ss.getSheetByName('Requests');
var data = sheet.getDataRange().getValues();
var requestID = grabRequestID();
for (var i=0; i < data.length; i++) { // going through all the rows
for (var j=0; j < data[i].length; j++) { // this is going through all the cell of a row
if (data[i][j] == requestID) {
Logger.log('found it');
var row = Number(i)+1;
var col = Number(j)+1+2;
while(sheet.getMaxColumns()<col){
sheet.insertColumnsAfter(sheet.getMaxColumns(),col-sheet.getMaxColumns());
}
sheet.getRange(row, col).setValue("approved");
}
}
}
}
function managersDecision2() {
var ss = SpreadsheetApp.openById(trackingSS)
var sheet = ss.getSheetByName('Requests');
var data = sheet.getRange("A:A").getValues()
var requestID = grabRequestID();
var col = 1+2;
while(sheet.getMaxColumns()<col){
sheet.insertColumnsAfter(sheet.getMaxColumns(),col-sheet.getMaxColumns());
}
for (var i=0; i < data.length; i++) { // going through all the rows
var row = 1+i;
if (data[i][0] == requestID) {
Logger.log('found it');
sheet.getRange(row, col).setValue("approved");
}
else if(data[i][0] !=""){
Logger.log(row)
sheet.getRange(row, col).setValue("denied");
}
}
}

Pull data from one Google spreadsheet to another using Google script

I have 2 spreadsheet in my Drive. I want to pull data from a cell in 1 spreadsheet and copy it in another.
The spreadsheet "TestUsage" will sometimes have data in column A, but none is column B.
I would like so that when I open the spreadsheet, it would populate that empty cell in sheet "TestUsage" from sheet "TestParts".
Here is my code:
var ssTestUsage = SpreadsheetApp.getActiveSpreadsheet();
var sTestUsage = ssTestUsage.getActiveSheet();
var lastRowTestUsage = sTestUsage.getLastRow();
var rangeTestUsage = sTestUsage.getSheetValues(1, 1, lastRowTestUsage, 4);
var TESTPARTS_ID = "1NjaFo0Y_MR2uvwit1WuNeRfc7JCOyukaKZhuraWNmKo";
var ssTestParts = SpreadsheetApp.openById(TESTPARTS_ID);
var sTestParts = ssTestParts.getSheets()[0];
var lastRowTestParts = sTestParts.getLastRow();
var rangeTestParts = sTestParts.getSheetValues(1, 1, lastRowTestParts, 3);
function onOpen() {
for (i = 2; i < lastRowTestUsage; i++) {
if (rangeTestUsage[i][0] !== "" && rangeTestUsage[i][1] == "") {
for (j = 1; j < lastRowTestParts; j++) {
if (rangeTestUsage[i][0] == rangeTestParts[j][0]) {
Logger.log(rangeTestUsage[i][1]);
Logger.log(rangeTestParts[j][1]);
rangeTestUsage[i][1] = rangeTestParts[j][1];
break;
}
}
}
}
}
The problem with this is this doesn't do anything:
rangeTestUsage[i][1] = rangeTestParts[j][1];
I know there must be a method that can get data from one range to another.
Please let me know if I am totally incorrect or I am on the right path.
the statement
"this doesn't do anything:"
rangeTestUsage[i][1] = rangeTestParts[j][2];
is not really true... and not really false neither..., actually it does assign the value to rangeTestUsagei but you dont see it because it is not reflected in the spreadsheet.
Both values are taken from the Sheet using getValues so at that time they are both array elements.
What is missing is just writing back the array to the sheet using the symetrical statement setValues()
Give it a try and don't hesitate to come back if something goes wrong.
EDIT :
I didn't notice at first that you were using getSheetValues instead of getValues (simply because I never use this one)... the only difference is that getValues is a method of the range class while yours belongs to the sheet class; the syntax is similar in a way, just use
Sheet.getRange(row,col,width,height).getValues()
it takes one word more but has the advantage to have a direct equivalent to set values
Sheet.getRange(row,col,width,height).setValues()
Serge insas has a good explanation of why your code doesn't work and hints at the solution below.
I recommend you use an array to store the updated values of column B that you want then set the entire column at the end.
Modifying your code...
var ssTestUsage = SpreadsheetApp.getActiveSpreadsheet();
var sTestUsage = ssTestUsage.getActiveSheet();
var lastRowTestUsage = sTestUsage.getLastRow();
var rangeTestUsage = sTestUsage.getSheetValues(1, 1, lastRowTestUsage, 2);
var TESTPARTS_ID = "1NjaFo0Y_MR2uvwit1WuNeRfc7JCOyukaKZhuraWNmKo";
var ssTestParts = SpreadsheetApp.openById(TESTPARTS_ID);
var sTestParts = ssTestParts.getSheets()[0];
var lastRowTestParts = sTestParts.getLastRow();
var rangeTestParts = sTestParts.getSheetValues(1, 1, lastRowTestParts, 2);
var colB = [];
function onOpen() {
for (i = 2; i < lastRowTestUsage; i++) {
if (rangeTestUsage[i][0] !== "" && rangeTestUsage[i][1] == "") {
var matched = false;
for (j = 1; j < lastRowTestParts; j++) {
if (rangeTestUsage[i][0] == rangeTestParts[j][0]) {
//Logger.log(rangeTestUsage[i][1]);
//Logger.log(rangeTestParts[j][1]);
colB.push([rangeTestParts[j][1]]); // push the value we want into colB array
matched = true;
break;
}
}
if(!matched) // this is in case you don't have a match
colB.push([""]); // incase we don't have a matching part
} else {
colB.push([rangeTestUsage[i][0]]); // we already have a value we want so just add that to colB array
}
}
sTestUsage.getRange(2,2,lastRowTestUsage).setValues(colB); // update entire column b with values in colB array
}

JavaScript delete merged table cell

I have been working on a scheduling website for the past few weeks. I am showing the schedules as PHP generated html-tables. I use merged cells for showing events. I have come to a problem when trying to delete events using JS. Since those are merged cells, using rowspan, I have to go through the table and re-adding empty cells whenever there is a need when I delete one. My solution is working fine for when my table contains one merged cell among nothing but empty cells, but with a more complex table, it fails. I can't really grasp what's wrong with it, except that it doesn't correctly find the cellIndex anymore. Does anyone have a clue? Here is what I'm talking about:
http://aturpin.mangerinc.com/table.html
(Click on an event to remove it, or attempt to anyhow)
This sample may help you find your solution. It seems to demonstrate your problem as well as have some sample code to generate a matrix to help you solve it.
EDIT: I liked the puzzle and decided to play with it for a bit, here is a "functioning" example of implementing that sample (although sometimes the table doesn't seem to redraw correctly. This should probably help you get further along the way.
function getTableState(t) {
var matrix = [];
var lookup = {};
var trs = t.getElementsByTagName('TR');
var c;
for (var i=0; trs[i]; i++) {
lookup[i] = [];
for (var j=0; c = trs[i].cells[j]; j++) {
var rowIndex = c.parentNode.rowIndex;
var rowSpan = c.rowSpan || 1;
var colSpan = c.colSpan || 1;
var firstAvailCol;
// initalized the matrix in this row if needed.
if(typeof(matrix[rowIndex])=="undefined") { matrix[rowIndex] = []; }
// Find first available column in the first row
for (var k=0; k<matrix[rowIndex].length+1; k++) {
if (typeof(matrix[rowIndex][k])=="undefined") {
firstAvailCol = k;
break;
}
}
lookup[rowIndex][c.cellIndex] = firstAvailCol;
for (var k=rowIndex; k<rowIndex+rowSpan; k++) {
if(typeof(matrix[k])=="undefined") { matrix[k] = []; }
var matrixrow = matrix[k];
for (var l=firstAvailCol; l<firstAvailCol+colSpan; l++) {
matrixrow[l] = {cell: c, rowIndex: rowIndex};
}
}
}
}
// lets build a little object that has some useful funcitons for this table state.
return {
cellMatrix: matrix,
lookupTable: lookup,
// returns the "Real" column number from a passed in cell
getRealColFromElement: function (cell)
{
var row = cell.parentNode.rowIndex;
var col = cell.cellIndex;
return this.lookupTable[row][col];
},
// returns the "point" to insert at for a square in the perceived row/column
getPointForRowAndColumn: function (row,col)
{
var matrixRow = this.cellMatrix[row];
var ret = 0;
// lets look at the matrix again - this time any row that shouldn't be in this row doesn't count.
for (var i=0; i<col; i++)
{
if (matrixRow[i].rowIndex == row) ret++;
}
return ret;
}
};
}
function scheduleClick(e)
{
if (e.target.className != 'event')
return;
//Get useful info before deletion
var numRows = e.target.rowSpan;
var cellIndex = e.target.cellIndex;
var rowIndex = e.target.parentNode.rowIndex;
var table = e.target.parentNode.parentNode;
var tableState = getTableState(table);
var colIndex = tableState.getRealColFromElement(e.target);
//Deletion
e.target.parentNode.deleteCell(cellIndex);
//Insert empty cells in each row
for(var i = 0; i < numRows; i++)
{
var row = table.rows[rowIndex + i];
row.insertCell(tableState.getPointForRowAndColumn(rowIndex+i, colIndex));
}
}

Categories