Restarting a COUNTIF function depending on multiple criteria - google-apps-script

I have a sheet with 4 columns, as shown below:
1
Date
Item Name
Counter
Flag
3
Date 1
Item A
1
4
Date 1
Item B
1
5
Date 2
Item B
2
6
Date 3
Item A
2
1
7
Date 3
Item B
3
8
Date 4
Item A
1
9
Date 5
Item A
2
Currently, I'm using a countif function [=countif(B$2:B2,B2)] to count the number of times a specific item appears in the spreadsheet. However, I need to find a way to restart the counter if there is a 1 in column D. In this case, this would mean that the formula in row 8 column C would be [=COUNTIF(B$8:B8,B8)] and would continue counting until it finds another row with a 1 in column D (e.g., formula in column C row 9 would be =COUNTIF(B$8:B9,B9). It would also ideally check whether there is a prior row with a 1 in column D, not through the order of the sheet, but by checking that it's date is smaller (and yet the closest date with a 1 in column D).
I've written the following script, which sets the row with a 1 in column D to 0 and sets the countif for the starting rows correctly to [=countif(B$2:B2,B2)], but it sets any row after there is a row with a 1 in column D as the same formula, with the starting range at B$2.
function setCountifFormula() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Test");
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) { //iterate through each row
var colBValue = data[i][1]; //get columnB in i
var colAValue = data[i][0]; // get date in i
var colDValue = data[i][3]; // get flag in i
var closestRow = 1; // empty variable
if( colDValue == "1") { //if columnD = 1
sheet.getRange(i+1,3).setValue(0); // set columnC = 0
} else {
for (var j = 1; j < data.length; j++) { //iterate through other rows
if (data[j][1] === colBValue && data[j][3] === "1") { // if columnB in j = ColumnB in i, and flag in row j = 1
var dateToCompare = data[j][0]; //set datetoCompare = date in row j
closestRow = j;
if (dateToCompare < colAValue) {
var range = "B$" + (closestRow + 1) + ":B" + (i + 1);
var formula = "=COUNTIF(" + range + ",B" + (i + 1) + ")";
sheet.getRange(i + 1, 3).setFormula(formula);
} else {
var range = "B$2:B" + (i+1);
var formula = "=COUNTIF(" + range + ",B" + (i+1) + ")";
sheet.getRange(i+1, 3).setFormula(formula);
}
}
}
if (closestRow === 1) {
var range = "B$2:B" +(i+1);
var formula = "=COUNTIF("+range +",B"+(i+1)+")";
sheet.getRange(i+1,3).setFormula(formula);
}
}
}
}
I can post the spreadsheet if needs be. If there is a different way without using scripts or COUNTIF, it'd be appreciated. Thanks!

I'm much better at scripting than complex formulas so here is an example of how I would do it.
function myCountif() {
try {
let values = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
values.shift(); // remove headers
let unique = values.map( row => row[1] );
unique = [...new Set(unique)];
let count = unique.map( row => 0 );
let counts = values.map( row => 0 );
values.forEach( (row,rIndex) => {
let cIndex = unique.findIndex( item => item === row[1] );
count[cIndex] = count[cIndex]+1;
counts[rIndex] = count[cIndex];
if( row[3] === 1 ) count[cIndex] = 0;
}
)
return counts;
}
catch(err) {
console.log(err);
}
}
Reference
Array.shift()
Array.map()
Set Object
Array.forEach()
Array.findIndex()
Arrow function =>

Related

Trying to Set Value in For Loop - apps script

I am trying to set the value of a cell in a column when two other columns at an index match values. How can I set the value using an index? (<Edited)
for (let i = 0; i < assetId.length; i++) {
for (let p = 0; p < oldId.length; p++) {
if (assetId[i] !="" && oldId[p] !="") {
if (assetId[i] == oldId[p]) {
Logger.log('Old Match: ' + assetId[i])
//if match modify 4th column at row [i] to 'null'
d.getRange(i,3).setValue('null')
}
}
}
}
Based on if assetId[i] == oldId[p], I am trying to change column F of row [i] to 'null'
Edit (examples requested)
Column J is oldId and K is newId
EXPECTED OUTPUT: F4 should be null
Full code:
function replaceIds() {
const ss = SpreadsheetApp.getActiveSpreadsheet()
const r = ss.getSheetByName("Form Responses 1")
const d = ss.getSheetByName("Devices")
const oldId = r.getRange("J2:J").getValues().flat()
const newId = r.getRange("K2:K").getValues().flat()
const studentName = r.getRange("C2:C").getValues().flat()
const assetId = d.getRange("G3:G").getValues().flat()
const annotatedUser = d.getRange("E3:E").getValues().flat()
for (let i = 0; i < assetId.length; i++) {
for (let p = 0; p < oldId.length; p++) {
if (assetId[i] !="" && oldId[p] !="") {
if (assetId[i] == oldId[p]) {
Logger.log('Old Match: ' + assetId[i])
//if match modify 4th column at row [i] to 'null'
d.getRange(i,3).setValue('null')
}
}
}
//new asset ID loop
for (let r = 0; r < newId.length; r++) {
//Logger.log(oldId[p])
if (assetId[i] !="") {
if (newId[r] !="") {
//Logger.log('## not null ##')
if (assetId[i] == newId[r]) {
Logger.log('New Match: ' + assetId[i])
}
}
}
}
}
}
Issue:
Issue is that, using a nested for loop is not a good idea as you can't properly follow where the proper index is, and it will also needlessly reiterate on items that were already visited.
Solution:
Looping only on the assetId should suffice, then using indexOf as it will help you identify if a certain element (current assetId) belongs in an array (list of oldIds).
If assetId is found, indexOf will return a non-negative number (which is what index the element is found in the array).
Exclude empty assetIds due to how you get your data
Then you can remove the column of that same row, but since index starts at 0 and your data starts at 3rd row, we need to offset the getRange row so it would match the cell we want to delete properly.
Modifying your current solution, this is what the solution says above, and should work.
Script:
function replaceIds() {
const ss = SpreadsheetApp.getActiveSpreadsheet()
const r = ss.getSheetByName("Form Responses 1")
const d = ss.getSheetByName("Devices")
const oldId = r.getRange("J2:J").getValues().flat()
const newId = r.getRange("K2:K").getValues().flat()
const studentName = r.getRange("C2:C").getValues().flat()
const assetId = d.getRange("G3:G").getValues().flat()
const annotatedUser = d.getRange("E3:E").getValues().flat()
// loop your assetId
assetId.forEach(function(cell, index){
// if assetId is listed under oldId, remove annotated location of that row
// also, skip any rows where assetIds are blank
if(oldId.indexOf(cell) > -1 && cell != "")
// offset here is 3 since assetId starts at G3 and index starts at 0
// 3 - 0 = 3, which is the offset, and 6 is column F
d.getRange(index + 3, 6).setValue('');
});
}
Output:
This function will change the value in column1 if the value of col2 at that index is in column 10 on any line. you can change the indices as you desire.
function findDataBasedOnMatch() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet0');
const sr = 2;//data start row
const vs = sh.getRange(sr, 1, sh.getLastRow() - sr + 1, sh.getLastColumn()).getValues();
const col10 =vs.map(r => r[9]);//you pick the indices
vs.forEach((r,i) => {
if(~col10.indexOf(r[1])) {//you pick the indices
sh.getRange(i + sr, 1).setValue('');
}
});
}

Increment a value in range ( one column ) with if and multicriteria - Google Apps Script

I try to loop on each row and check for U & AA values and set value of the Y value of the same row based on the folowing criteria :
J value is 1
If AA value is : "TEST" then Y value is set ( j + 1 )
If U value is : "33" then Y value is set ( j + 1 ) then J = j + 1
If AA value isn't "TEST" & U value isn't "33" then Y value is set ( j )
Here my code
function RowNumbre() {
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getSheetByName('Test');
var range = sheet.getRange("AA:AA200").getValues();
for (var i=2; i < range.length; i++) {
var j = 1;
var value = sheet.getRange("Y"+i);
var value2 = sheet.getRange("U"+i).getValue();
var value3 = sheet.getRange("AA"+i).getValue();
if(value2 === "33"){
value.setValue(j+1);
j= j+1;
}
else if(value3 === "TEST"){
value.setValue(j+1);
}
else {
value.setValue(j);
}
}
}
The code don't work at all and it's look like there is a huge lag ( for the moment, a "1" is added to each cell on Y each 2-3 minutes and it wont stop.
Try this:
function RowNumbre() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('Test');
const rg1=sh.getRange("Y1:Y200");
var vs1=rg1.getValues();
const rg2=sh.getRange("U1:U200");
var vs2=rg2.getValues();
const rg3=sh.getRange("AA1:AA200");
var vs3=rg3.getValues();
var sv=1;
vs1.forEach(function(v,i){if(vs2[i][0]==33){v[0]=sv++;}else if(vs3[i][0]=="TEST"){v[0]=sv+1;}else{v[0]=sv;}});
rg1.setValues(vs1);
}

How to deal with duplicates in google sheets script?

So in the project I want to do I have a google sheet with timestamps and names next to those timestamps in the spreadsheet. I am having trouble accounting for duplicates and giving the name multiple timestamps in another google sheet.
for(var i = 0; i < length; i++){//for loop 1
if(currentCell.isBlank()){//current cell is blank
daySheet.getRange(i+10, 2).setValue(fullName);//set name to first cell
daySheet.getRange(i+10,3).setValue(pI);
daySheet.getRange(i+10,day+3).setValue(1);
}else if(counter > 1 ){//the index of the duplicate in the sheet month
//if counter is > 1 then write duplicates
for(var t = 1; t <= sheetLength ; t++){//loop through sign in sheet
//current index i
if(signInLN == signInSheet.getRange(t+1,3).getValue()){
//if there is a match
daySheet.getRange(t+10,day+3).setValue(1);
//day is equal to the day I spliced from the timestamp
//at this point I am confused on how to get the second date that has the same
//name and add to the row with the original name.
//when i splice the timestamp based on the row of index i, with duplicates I get
//the day number from the first instance where the name is read
}
}
}//for loop 1
How can I get this to work with duplicates so I can account for the dates but make sure that if there are
any duplicates they will be added to the row of the original name
Google Sheet EX:
12/10/2020 test1
12/11/202 test2
12/15/2020 test1
Should be something like this:
name 10 11 12 13 14 15 16
test1 1 1
test2 1
//the one is to identify that the date is when the user signed in on the sheets.
Sample Spreadsheet:
Code snippet done with Apps Script, adapt it to your needs.
use Logger.log() in case you don't understand parts of code
It is done mainly with functional JavaScript
function main(){
var inputRange = "A2:B";
var sheet = SpreadsheetApp.getActive().getSheets()[0]
var input = sheet.getRange(inputRange).getValues(); //Read data into array
var minDate, maxDate;
var presentDates = input.map(function(row) {return row[0];}); //Turns input into an array of only the element 0 (the date)
minDate = presentDates.reduce(function(a,b) { return a<b?a:b}); //For each value, if its the smallest: keep; otherwise: skip;
maxDate = presentDates.reduce(function(a,b) { return a>b?a:b}); //Same as above, but largest.
var dates = [];
for (var currentDate = minDate; currentDate <= maxDate; currentDate.setDate(currentDate.getDate()+1)) {
dates.push(getFormattedDate(currentDate)); //Insert each unique date from minDate to maxDate (to leave no gaps)
}
var uniqueNames = input.map(function(row) {return row[1];}) //Turns input into an array of only the element at 1 (the names)
.filter(function (value, index, self) {return self.indexOf(value) === index;}); //Removes duplicates from the array (Remove the element if the first appearence of it on the array is not the current element's index)
var output = {}; //Temporary dictionary for easier counting later on.
for (var i=0; i< dates.length; i++) {
var dateKey = dates[i];
for (var userIndex = 0; userIndex <= uniqueNames.length; userIndex++) {
var mapKey = uniqueNames[userIndex]+dateKey; //Match each name with each date
input.map(function(row) {return row[1]+getFormattedDate(row[0])}) //Translate input into name + date (for easier filtering)
.filter(function (row) {return row === mapKey}) //Grab all instances where the same date as dateKey is present for the current name
.forEach(function(row){output[mapKey] = (output[mapKey]||0) + 1;}); //Count them.
}
}
var toInsert = []; //Array that will be outputted into sheets
var firstLine = ['names X Dates'].concat(dates); //Initialize with header (first element is hard-coded)
toInsert.push(firstLine); //Insert header line into output.
uniqueNames.forEach(function(name) {
var newLine = [name];
for (var i=0; i< dates.length; i++) { //For each name + date combination, insert the value from the output dictionary.
var currentDate = dates[i];
newLine.push(output[name+currentDate]||0);
}
toInsert.push(newLine); //Insert into the output.
});
sheet.getRange(1, 5, toInsert.length, toInsert[0].length).setValues(toInsert); //Write the output to the sheet
}
// Returns a date in the format MM/dd/YYYY
function getFormattedDate(date) {
var year = date.getFullYear();
var month = (1 + date.getMonth()).toString();
month = month.length > 1 ? month : '0' + month;
var day = date.getDate().toString();
day = day.length > 1 ? day : '0' + day;
return month + '/' + day + '/' + year;
}
Run script results:

Sum up the time values corresponding to same date

In my sheet column A is date and column B is time duration values, I want to find the dates which are repeated and sum up the corresponding time values of the repeated dates and show the sum in the last relevant repeated date. And delete all the other repeated dates. ie if 18/07/2019 is repeated 4 times i have to sum up all the four duration values and display the sum value in the 4th repeated position and delete the first three date 18/07/2019. I have to do this all those dates that are repeated. I have wrote code to my best knowledge
function countDate() {
var data = SpreadsheetApp.getActive();
var sheet = data.getSheetByName("Sheet5");
var lastRow = sheet.getLastRow();
var sh = sheet.getRange('A1:A'+lastRow);
var cell = sh.getValues();
var data= sheet.getRange('B1:B'+lastRow).getValues();
for (var i =0; i < lastRow; ++i){
var count = 0;
var column2 = cell[i][0];
for (var j =0; j < i; j++)
{
var p=0;
var column4 = cell[j][0];
if (column4 - column2 === 0 )
{
var value1 = data[j][0];
var value2 = data[i][0];
var d = value2;
d.setHours(value1.getHours()+value2.getHours()+0);
d.setMinutes(value1.getMinutes()+value2.getMinutes());
sheet.getRange('C'+(i+1)).setValue(d).setNumberFormat("[hh]:mm:ss");
sheet.deleteRow(j+1-p);
p++;
}
}
}
}
The copy of the sheet is shown
column C is the values I obtain through the above code AND column D is the desired value
After computing the sum I need to delete the repeated rows till 15 here
Answer:
You can do this by converting your B-column to a Plain text format and doing some data handling with a JavaScript dictionary.
Code:
function sumThemAllUp() {
var dict = {};
var lastRow = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getLastRow();
var dates = SpreadsheetApp.getActiveSpreadsheet().getRange('A1:A' + lastRow).getValues();
var times = SpreadsheetApp.getActiveSpreadsheet().getRange('B1:B' + lastRow).getValues();
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
sheet.getRange(1, 1, sheet.getLastRow(), sheet.getLastColumn()).setNumberFormat("#");
for (var i = 0; i < dates.length; i++) {
if (!dict[dates[i][0]]) {
dict[dates[i][0]] = times[i][0];
}
else {
var temp = dict[dates[i][0]];
var hours = parseInt(temp.split(':')[0]);
var minutes = parseInt(temp.split(':')[1]);
var additionalHours = parseInt(times[i][0].split(':')[0]);
var additionalMinutes = parseInt(times[i][0].split(':')[1]);
var newMinutes = minutes + additionalMinutes;
var newHours = hours + additionalHours;
if (newMinutes > 60) {
newHours = newHours + 1;
newMinutes = newMinutes - 60;
}
dict[dates[i][0]] = newHours + ':' + newMinutes;
}
}
SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getRange('A1:B' + lastRow).clear();
var keys = Object.keys(dict);
for (var i = 0; i < keys.length; i++) {
SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getRange('A' + (i + 1)).setValue(keys[i]);
SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getRange('B' + (i + 1)).setValue(dict[keys[i]]);
}
}
Assumptions I made:
There are a few assumptions I made when writing this, you can edit as needed but I figured I should let you know:
There are only dates in Column A and only times in Column B.
The times in column B are either Hours:Minutes or Minutes:Seconds. Either way, if the value to the right of the : hits 60, it adds one to the left value and resets.
The Sheet within the Spreadsheet is the first sheet; that which is returned by Spreadsheet.getSheets()[0].
References:
w3schools - JavaScript Objects
Spreadsheet.getSheets()
w3schools - JavaScript String split() Method
MDN web docs - parseInt() method
Google Sheets > API v4 - Date and Number Formats

Find LastRow of column C (when Col A and B have a different row size)?

How to find the last used cell of column C ?
Example: "Sheet1" : "Col A" and "Col B" have 1200 rows. And "Col C" has only 1 row.
## ColA ColB ColC
## 1 1 1
## 2 2 empty
## .. .. ..
## 1200 1200 empty
Here are my unsuccessful tests :
Function find_last_row_other_column() {
var ws_sheet =
var ws = SpreadsheetApp.openById("Dy...spreadsheet_id...4I")
var ws_sheet = ws1.getSheetByName("Sheet1");
var lastRow = ws_sheet.getRange("C").getLastRow();
var lastRow = ws_sheet.getRange("C:C").getLastRow();
var lastRow = ws_sheet.getRange(1,3,ws_sheet.getLastRow()); 1200 rows for colA! instead of row = 1 for col C.
}
Note: I can't use C1 because next time I use the function it will be C1200 or something else.
var lastRow = ws_sheet.getRange("C1").getLastRow();
I ask this because my next goal is to copy/paste the result of C1 into C2:C1200. Here is my test :
var lastRow = ws_sheet.getLastRow();
var target_range = ws_sheet.getRange(1,3,lastRow,1); //C1 until last row
var Formula_values = source_range.getValues();
target_range.setValues(Formula_values);
Thanks in advance ;)
ps: I have spend 2 hours on it. I have tried similar problems & their solutions already given on this website, but I can't happen to make them working. I am lost ! :
More efficient way too look up the last row in a specific column?
and Get last row of specific column function - best solution
As I mentioned in the comments above, this is the subject of the highest score post on StackOverFlow...
The original post returns the value of the last cell in a column but a (very) little modification makes it return the row index.
Original post :
Script:
function lastValue(column) {
var lastRow = SpreadsheetApp.getActiveSheet().getMaxRows();
var values = SpreadsheetApp.getActiveSheet().getRange(column + "1:" + column + lastRow).getValues();
for (; values[lastRow - 1] == "" && lastRow > 0; lastRow--) {}
return values[lastRow - 1];
}
modified to return index of the last used cell in a column :
function lastValue(column) {
var lastRow = SpreadsheetApp.getActiveSheet().getMaxRows();
var values = SpreadsheetApp.getActiveSheet().getRange(column + "1:" + column + lastRow).getValues();
for (; values[lastRow - 1] == "" && lastRow > 0; lastRow--) {}
return lastRow;
}
Here is the function to do it:
function lastRowInColumnLetter(column) {
var lastRow = SpreadsheetApp.getActiveSheet().getLastRow() - 1; // values[] array index
var values = SpreadsheetApp.getActiveSheet().getRange(column + "1:" + column + (lastRow + 1)).getValues();
while (lastRow > -1 && values[lastRow] == "") {
lastRow--;
}
if (lastRow == -1) {
return "Empty Column";
} else {
return lastRow + 1;
}
}
and you invoke it as =lastRowInColumnLetter("C").
And here are 3 more useful functions in this context:
function lastValueInColumnLetter(column) {
var lastRow = SpreadsheetApp.getActiveSheet().getLastRow() - 1; // values[] array index
var values = SpreadsheetApp.getActiveSheet().getRange(column + "1:" + column + (lastRow + 1)).getValues();
while (lastRow > -1 && values[lastRow] == "") {
lastRow--;
}
if (lastRow == -1) {
return "Empty Column";
} else {
return values[lastRow];
}
}
function lastValueInColumnNumber(column) {
var lastRow = SpreadsheetApp.getActiveSheet().getLastRow() - 1; // values[] array index
var values = SpreadsheetApp.getActiveSheet().getRange(1,column,lastRow + 1).getValues();
while (lastRow > -1 && values[lastRow] == "") {
lastRow--;
}
if (lastRow == -1) {
return "Empty Column";
} else {
return values[lastRow];
}
}
function lastRowInColumnNumber(column) {
var lastRow = SpreadsheetApp.getActiveSheet().getLastRow() - 1; // values[] array index
var values = SpreadsheetApp.getActiveSheet().getRange(1,column,lastRow + 1).getValues();
while (lastRow > -1 && values[lastRow] == "") {
lastRow--;
}
if (lastRow == -1) {
return "Empty Column";
} else {
return lastRow + 1;
}
}
These functions properly address empty columns, and also start counting backwards from the last row with content on the active sheet getLastRow(), and not from the last row on the sheet (with or without content) getMaxRows() as in the accepted answer.
If you don't have empty cells between your data, you can use this:
function last_Column_Row(){
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
var Direction = SpreadsheetApp.Direction;
var xcol = 2;//e.g. for column 2 ("B"), to obtain its last row
var yrow = 8;//e.g. for row 8, to obtain its last column
var lastRow =sheet.getRange(1,xcol).getNextDataCell(Direction.DOWN).getRow();//last row of column 'xcol'
var lastCol =sheet.getRange(yrow,1).getNextDataCell(Direction.NEXT).getColumn();//last column of row 'yrow'
};
It gets the number of next empty cell-1 of a specific row or column (similar to Ctrl + 'arrow' in a sheet)
But If you have empty cells between your data, you can use this:
function last_Row_Column2()
{
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
var Direction = SpreadsheetApp.Direction;
var maxR =sheet.getMaxRows();
var maxC = sheet.getMaxColumns();
var yrow = 8;//e.g. for row 8, to obtain its last column
var xcol = 2;//e.g. for column 2 ('B'), to obtain its last row
var valMaxR = sheet.getRange(maxR,xcol).getValue();//for the case that the last row has the last value
var valMaxC = sheet.getRange(yrow,maxC).getValue();//for the case that the last column has the last value
if(valMaxR !=''){var lastRow = maxR;}//if the last row in studied column is the last row of sheet
else{var lastRow =sheet.getRange(maxR,xcol).getNextDataCell(Direction.UP).getRow();}
if(valMaxC !=''){var lastCol = maxC;}//if the last column in studied row is the last column of sheet(e.g.'Z')
else{var lastCol =sheet.getRange(yrow,maxC).getNextDataCell(Direction.PREVIOUS).getColumn();}
};
[UPADTE} Please disregard this answer. User Serge's code instead. I was having a brain fart. His answer is magnitudes better in every way. That will teach me not to answer SO questions after you come back from a cocktail night... [/UPDATE]
The following function will log the last non-blank row number of column C. Note: if, for example, column C has a value in row 1 and row 200, with rows 2-199 blank, the function will return 200 as last non-blank row - it does not account for blank rows above last non-blank row.
function getLastNonBlankColCrow() {
var sheet = SpreadsheetApp.getActiveSheet();
var lastNonBlankColCrow = 0;
for (var i=1, lenRows=sheet.getRange("C:C").getNumRows(); i<=lenRows; i++) {
if ( !sheet.getRange(i, 3).isBlank() ) { // 3 is 1-based index of column C
lastNonBlankColCrow = i;
}
}
Logger.log(lastNonBlankColCrow);
}