Function to check for duplicates based on condition - google-sheets

I have a simple Google sheet that records what sessions people are signed up for (3 concurrent sessions per day):
The same person cannot be in more than 1 session on a given day. I'd like to create a function in column B that checks for that situation and flags it, as in Susan, Keith, and Amy in the example above (I've highlighted in yellow the conditions that would trigger a flag).
If there were just one date, I'd use a countif (or maybe countifs?) to check for more than 1 TRUE for that date. But with multiple dates, I think some sort of iterative function or query is needed. I have a feeling I may be missing a simple formula, but it's eluding me. I may add more dates, so the solution needs to allow for n number of dates in the range.
UPDATE: My scenario has become a little more complex. I'm designating a potential role each person can play in each session and then using the checkboxes to indicate who is playing what role in each session. A given person can't be in more than 1 session per day (but a given person may be in 0 sessions on a given day). The below image shows this updated scenario, with the yellow highlights showing the conditions that I want flagged via the function in column B.
Here's a link to the Google sheet if you want to create a copy.

Given the use case provided, you can apply the formula below to B3 and drag the auto-complete handle:
=IF(ARRAYFORMULA(SUM(INT(C3:K3))) = COUNTUNIQUE($C$1:$1), "", "FLAG")
I'm converting the Boolean values to INT and summing them up. If the sum is equal to the count of unique days in the first row, then everything is fine, otherwise, FLAG!
In other words, if there are more (or less) checks than days, it should be flagged.
You can also set up a conditional formatting to paint the cell accordingly.
Alternatively, if you’d like to treat each scenario you can use =IFS() as below:
=IFS(ARRAYFORMULA(SUM(INT(C3:K3))) > COUNTUNIQUE($C$1:$1), "HIGHER", ARRAYFORMULA(SUM(INT(C3:K3))) < COUNTUNIQUE($C$1:$1), "LOWER", ARRAYFORMULA(SUM(INT(C3:K3))) = COUNTUNIQUE($C$1:$1), "OK")
References:
Sheets Functions documentation
IF
IFS
ARRAYFORMULA
SUM
INT
COUNTUNIQUE
EDIT:
Since the changes in the original scope significantly impacted my previous answer, here is a suggestion using a custom formula:
function checkFlags(){
var ss = SpreadsheetApp.getActive(); // get active Sheets
var ws = ss.getSheetByName("Sheet1"); // getting tab named "Sheet1"
var currentCellRange = ss.getActiveRange(); // getting active cell, in the context of a custom formula, it gets the one being calculated at the time
var rowIndex = currentCellRange.getRowIndex(); //getting current row number
var rowValues = ws.getRange(`${rowIndex}:${rowIndex}`).getValues()[0]; //getting row cells values
var sessionsList = []; //temp variable to store useful data from cells
for (var i = 0; i < rowValues.length; i++) { //reading cells on the row to create a date/flag array
var cell = rowValues[i]; //getting Range of current cell
if (typeof(cell) == 'boolean'){ //if the current cell has a boolean value, it is a session flag
var headerDate = ws.getRange(2, (i+2)).getDisplayValue(); //getting the header value on row 2 (current date for the session flag)
sessionsList.push({date: headerDate, session: cell});//storing date and session flag value on the temp variable
}
};
var groupBy = function(xs, key) { //handle function to proccess the sessionsList variable and group flag values by 'date'
return xs.reduce(function(rv, x) {
(rv[x[key]] = rv[x[key]] || []).push(x);
return rv;
}, {});
};
var tempGroupedArray = groupBy(sessionsList, 'date'); //grouping temp sessionsList by 'date'. This will return an array like [ { date: '<date>', session: true/false }, { date: '<date>', session: true/false }, ...]
for (dateFlags of Object.entries(tempGroupedArray)) {//looping through the `grouped by date` array
var tempCount = dateFlags[1].filter(x => x.session==true).length; //how many `trues` are for this date
if (tempCount > 1){ //if there is more than one session set as "true" for this date...
return 'FLAG'; //...immediately stop execution and return FLAG
}
};
//if it passed the loop above, it means there is no flags
return ''; //then return blank
}
NOTES: This custom formula will not update the result in the cell after a change on the flag values, you must delete/paste it to force if wanted.

Related

Add checkbox to lock cells with dropdown lists in Google Sheets

Suppose I have a Google Sheet where columns A:G are dropdown lists.
I would like to add an option to lock choices in columns A:G using a checkbox in column H, where checking the box in this column will lock columns A:G so they can not be edited (unless the checkbox in column H is de-selected).
Is it possible to do this on Google Sheets?
I have created the following script that is based on a previous script I found in a response here in StackOverflow a long time ago.
function onEdit(e)
{
var editRange = {
top : 1,
bottom : 1000,
left : 2,
right : 2
};
var cell = e.range;
// Exit if we're out of range
var thisRow = cell.getRow();
if (thisRow < editRange.top || thisRow > editRange.bottom) return;
var thisCol = cell.getColumn();
if (thisCol < editRange.left || thisCol > editRange.right) return;
var val1 = e.range.getRow();
var val2 = e.range.getColumn();
var validation = SpreadsheetApp.getActive().getSheetByName("Testing Sheet").getRange(val1, val2-1);
if(cell.isChecked()){
var rule = SpreadsheetApp.newDataValidation().requireValueInList(['Red', 'Blue']).build();
}
else{
var rule = SpreadsheetApp.newDataValidation().requireValueInList(['Black', 'White']).build();
}
validation.setDataValidation(rule);
}
I modified the range at the beginning so it only recognizes the change in column B, but you can just change it to 8 for column H. Since the script uses an onChange trigger, what you need to validate first is if the edit was made in the range where the checkboxes are, then I set the range of the cell that I was going to change based on the value from the checkbox and created the data validation rule with the expected values depending on the conditional.
In your case you can just add the rest of the ranges for the other columns with more validation variables and change the column parameter to -2, -3, or -4 depending on the cells you will be editing for example, and then create more validation rules according to the data you need in the dropdowns.
Here is an example of how this would work:
References:
onEdit()
Data Validation

Problem with recording data change in google sheet

I have two sheets. On one sheet (Sheet1), I have a column 'A' which has 5 fixed values, and a column 'B' which records the timestamp value in 'A' change. The other sheet (Sheet2) is meant to record all the changes in the first sheet in one day.
I use this simple query in the recording sheet:
=QUERY(Sheet1!A$1:X,"select * where C>="& Sheet2!D1)
with Sheet1!C has timestamps and Sheet2!D1 is the timestamp of 12:00 AM today
The problem is when I change the value of a row in C columns more than one time, instead of creating a new row in Sheet2 it change the value of that row in Sheet2 into new values.
So how do I change my code to get my desire results?
EDIT 2: here is my new code, but it doesn't help.
function importdata(x) {
// var row = x.range.getRow();
// var col = x.range.getColumn();
var addedsheet=SpreadsheetApp.getActive().getSheetByName("Change"); // Sheet where I want to keep the record of the change
var original_sheet = SpreadsheetApp.getActive().getSheetByName("Master"); //sheet where the change is happended
var compared_value = addedsheet.getRange(1,4).getValue(); // Cell D1 of sheet "Change", which has timestamp of today
var insert_area = original_sheet.getRange("A2:X").getValues() // area to get value from "Master" sheet to put into "Change"
var compared_area = original_sheet.getRange("C2:C").getValues(); // area where has timestamp
if (compared_area >= compared_value){
addedsheet.values.append([insert_area])}
} //if timestamp of one row from Master is greater than the value at Change!D1 =>append that row at the end (this is what I'm trying to do)
EDIT 3: I fixed the above code by append[insert_area][0] instead of [insert_area]
But then I have a new problem: there will a chance that a row in sheet 1 will be overwrited in sheet 2. I try something like this, but it returns nothing on the sheet.
function for_each_row(){
var addedsheet=SpreadsheetApp.getActive().getSheetByName("Change"); // Sheet where I want to keep the record of the change
var original_sheet = SpreadsheetApp.getActive().getSheetByName("Master"); //sheet where the change is happended
var compared_value = addedsheet.getRange(1,4).getValue(); // Cell D1 of sheet "Change", which has timestamp of today
var number_of_row_2 = addedsheet.getLastRow;
var number_of_row_1 = original_sheet.getLastRow();
for (var i=2; i<number_of_row_1 +1; i++){
var compared_stamp = original_sheet.getRange("C"+i).getValues();
var insert_values = (original_sheet.getRange(i,1,1,24).getValues())
if (compared_stamp > compared_value){
var insert_values = (original_sheet.getRange(i,1,1,24).getValues());
for (var j = 2; j<number_of_row_2 +1; j++){
var value_from_sheet = addedsheet.getRange(j,1,1,24).getValues();
if (insert_values ===value_from_sheet){
return
}
else(
addedsheet.appendRow(insert_values[0]))
}
}
}
}
My thought is if a row satisfies the 1st condition then the value will be check in sheet 2. If sheet 2 didn't have that row then append that row.
Issue:
If I understand you correctly, you want to do the following:
If sheet Master is edited, iterate through all rows in this sheet (excluding headers), and for each row, check the following:
Column C has a higher value than cell D1 in sheet Change.
This row, with these exact values, does not exist in Change.
If these conditions are meet, append the row to Change.
Solution:
Use filter and some to filter out rows that don't match your two conditions, and use setValues to write the resulting rows to your other sheet.
Code snippet:
function onEdit(e) {
var editedSheet = e ? e.range.getSheet() : SpreadsheetApp.getActiveSheet();
if (editedSheet.getName() === "Master") {
var addedSheet = SpreadsheetApp.getActive().getSheetByName("Change");
var compared_value = addedSheet.getRange(1,4).getValue();
var newData = editedSheet.getRange("A2:X" + editedSheet.getLastRow()).getValues();
var currentData = addedSheet.getRange("A2:X" + addedSheet.getLastRow()).getValues();
var filteredData = newData.filter(row => row[2] >= compared_value)
.filter(row => !currentData.some(currentRow => JSON.stringify(currentRow) === JSON.stringify(row)));
addedSheet.getRange(addedSheet.getLastRow()+1,1,filteredData.length,filteredData[0].length).setValues(filteredData);
}
}
Note:
An easy way to check if two rows have the same values is using JSON.stringify(row).
I'm assuming the timestamps are not Dates. If they are, you should compare them using getTime(). So you should change the corresponding code to newData.filter(row => row[2].getTime() >= compared_value.getTime()).

Finding lowest value with no overlapping dates

I have a spreadsheet with criteria, a start and end date, and a value. The goal is to find the lowest value for each unique criteria and start date without overlapping dates (exclusive of end date). I made a pivot table to make it easier for myself but I know there is probably a way to highlight all valid rows that meet the above requirements with some formula or conditional formatting.
I have attached a google drive link where the spreadsheet can be found here and I have some images of the sheet as well. I know that it might be possible with conditional formatting but I just don't know how to combine everything I want it to do in a single formula.
Example below:
Row 2 is a valid entry because it has the lowest value for Item 1 starting on 03-15-2021, same with row 9.
Row 5 is valid because the start date does not fall within the date range of row 2 (exclusive of end date)
Row 7 is not valid because the start date is between the start and end date of row 6
You may add a bounded script to your project. Then you can call it either with a picture/drawing that has the function assigned (button-like), or adding a menu to Google Sheets.
From what you said in the question and the comments, this seems to do what you are trying. Notice that this requires the V8 runtime (which should be the default).
function validate() {
// Get the correct sheet
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
const sheet = spreadsheet.getSheetByName('Sheet1')
// Get the data
const length = sheet.getLastRow() - 1
const range = sheet.getRange(2, 1, length, 4)
const rows = range.getValues()
const data = Array.from(rows.entries(), ([index, [item, start, end, value]]) => {
/*
* Row Index
* 1 Criteria 1
* 2 Item 1 0
* 3 Item 1 1
* 4 Item 1 2
*
* row = index + 2
*/
return {
row: index + 2,
criteria: item,
start: start.getTime(),
end: end.getTime(),
value: value
}
})
// Sort the data by criteria (asc), start date (asc), value (asc) and end date (asc)
data.sort((a, b) => {
let order = a.criteria.localeCompare(b.criteria)
if (order !== 0) return order
order = a.start - b.start
if (order !== 0) return order
order = a.value - b.value
if (order !== 0) return order
order = a.end - b.end
return order
})
// Iterate elements and extract the valid ones
// Notice that because we sorted them, the first one of each criteria will always be valid
const valid = []
let currentCriteria
let currentValid = []
for (let row of data) {
if (row.criteria !== currentCriteria) {
// First of the criteria
valid.push(...currentValid) // Move the valids from the old criteria to the valid list
currentValid = [row] // The new list of valid rows is only the current one (for now)
currentCriteria = row.criteria // Set the criteria
} else {
const startDateCollision = currentValid.some(valid => {
row.start >= valid.start && row.start < valid.end
})
if (!startDateCollision) {
currentValid.push(row)
}
}
}
valid.push(...currentValid)
// Remove any old marks
sheet.getRange(2, 5, length).setValue('')
// Mark the valid rows
for (let row of valid) {
sheet.getRange(row.row, 5).setValue('Valid')
}
}
Algorithm rundown
We get the sheet that we have the data in. In this case we do it by name (remember to change it if it's not the default Sheet1)
We read the data and transform it in a more an array of objects, which for this case makes it easier to manage
We sort the data. This is similar to the transpose you made but in the code. It also forces a priority order and groups it by criteria
Iterate the rows, keeping only the valid:
We keep a list of all the valid ones (valid) and one for the current criteria only (currentValid) because we only have to check data collisions with the ones in the same criteria.
The first iteration will always enter the if block (because currentCriteria is undefined).
When changing criteria, we dump all the rows in currentValid into valid. We do the same after the loop with the last criteria
When changing criteria, the CurrentValid is an array with the current row as an element because the first row will always be valid (because of sorting)
For the other rows, we check if the starting date is between the starting and ending date of any of the valid rows for that criteria. If it's not, add it to this criteria's valid rows
We remove all the current "Valid" in the validity row and fill it out with the valids
The cornerstone of the algorithm is actually sorting the data. It allows us to not have to search for the best row, as it's always the next one. It also ensures things like that the first row of a criteria is always valid.
Learning resources
Javascript tutorial (W3Schools)
Google App Scripts
Overview of Google Apps Script
Extending Google Sheets
Custom Menus in Google Workspace
Code references
Class SpreadsheetApp
Class Sheet
Sheet.getRange (notice the 3 overloads)
let ... of (MDN)
Spread syntax (...) (MDN)
Arrow function expressions (MDN)
Array.from() (MDN)
Array.prototype.push() (MDN)
Array.prototype.sort() (MDN)
Date.prototype.getTime() (MDN)
String.prototype.localeCompare() (MDN)

How to pass cell value to a SUM function

I am trying to do a SUM of last 12 rows in the column (I'll be adding more rows into this column so I wanted to automate the calculation).
First of all, I am able to get the value of last cell with some value in this column by =SUMPRODUCT(MAX((B1:B200<>"")*ROW(B1:B200))) - result is stored in C1. However, I am not sure how to use this value inside the SUM formula, I was thinking something like =SUM(B(get value of C1)-12:B(get value of C1).
I tried multiple things but none of them have worked - I also don't mind using a different approach if it gets the job done.
You can create your own custom function to do that using Google Apps Script (GAS).
Try the following:
function onEdit(e){
var row = e.range.getRow();
var col = e.range.getColumn();
if ( col==2 && e.source.getActiveSheet().getName() == "Sheet1" ){
e.source.getActiveSheet().getRange("C1").setValue(sumLast12());
}
}
function sumLast12() {
var sheet = SpreadsheetApp.getActive().getSheetByName("Sheet1");
var sheet_size = sheet.getLastRow();
var elmt = sheet.getRange("B1:B"+sheet_size).getValues().flat([1]);
var elmt12 = elmt.slice(-12);
var sum = 0;
for( var i = 0; i < elmt12.length; i++ ){
sum += parseInt( elmt12[i], 10 );
}
return sum;
}
Explanation:
In order to activate this functionality go the menu bar on top of the
spreadsheet file and click on Tools => Script editor and copy the
aforementioned code into a blank script document (see attached
screenshot for more information) and save the document (cntrl+s).
After the script has been saved, everytime you edit a cell in column
B (either by adding a new value on the bottom or modify an existing value, the script will automatically update the value in
cell C1 with the sum of the last 12 values in column B.
Note that if you don't want to change my code, name the sheet you are working with as Sheet1.
Does this work?
=SUM(FILTER(B:B,ROW(B:B)>=MAX(ROW(B:B))-12)

Generating invoice from Google Sheets

I have a Google spreadsheet in which I record my freelance jobs. I have it set up that each line calculates whether it is paid for. (Payments are pulled from a separate sheet.)
What I would like to do is to generate an invoice, where I would select the customer and I get a listing of all unpaid entries for that customer.
Using a arrayed filter function does the job, but I can't use that as an invoice because I need the total line underneath, and would prefer the table format matching the count of entries.
Is it possible to insert such information into a Google Doc as a table, or within Sheets, to push the lines following an array down?
I thought this would be a simple enough concept but I can't find anything that does the full deal.
You could try this script. I'm not sure if the final results is what you are looking for. In case it is not, it can be easily modified:
function onEdit(e) {
//If you change the Customer in the Invoice sheet, it runs the code
if (e.range.getA1Notation() == 'A1' && e.source.getSheetName() == 'Invoice'){
var sprsheet = SpreadsheetApp.getActiveSpreadsheet();
var invoice = sprsheet.getSheetByName("Invoice");
var times = sprsheet.getSheetByName("Times");
var in_customer = invoice.getRange("A1").getValue(); //Name you selected in the dropdown menu
var data = times.getRange("A1:H").getValues(); //All the data from the Time sheet
var total = 0;
//Loops through all the data looking for unpaid subtotals from that customer
for (var i = 0; i < data.length; i++){
/*> "i" represents the row, the second number is the column
> The rows start at 0 since it is the first array position.
*/
if (in_customer == data[i][2]) {
if (data[i][7] == 'N'){
total += Number(data[i][5]); //Accumulates each subtotal into total
invoice.appendRow([data[i][0], data[i][1], data[i][3], data[i][5]]);
}
}
}
invoice.appendRow(["Total: ","","", total]);
}
}
This results in (I changed some values to test it):
As you see I added some headers.
References:
Range Class
onEdit Trigger

Resources