In Google Sheets, I have a spreadsheet called Events/Incidents which staff from various branches populate. I want Column B to automatically generate a unique ID based on the year in column A and the previously populated event. Given that there could be several events on a particular day, rows in column A could have duplicate dates.
The following is an example of what I am looking for in column B:
There can be no duplicates. Would really appreciate some help with either code or formula.
There are my thoughts https://github.com/contributorpw/google-apps-script-snippets/blob/master/snippets/spreadsheet_autoid/autoid.js
The main function gets a sheet and makes the magic
/**
*
* #param {GoogleAppsScript.Spreadsheet.Sheet} sheet
*/
function autoid_(sheet) {
var data = sheet.getDataRange().getValues();
if (data.length < 2) return;
var indexId = data[0].indexOf('ID');
var indexDate = data[0].indexOf('DATE');
if (indexId < 0 || indexDate < 0) return;
var id = data.reduce(
function(p, row) {
var year =
row[indexDate] && row[indexDate].getTime
? row[indexDate].getFullYear() % 100
: '-';
if (!Object.prototype.hasOwnProperty.call(p.indexByGroup, year)) {
p.indexByGroup[year] = [];
}
var match = ('' + row[indexId]).match(/(\d+)-(\d+)/);
var idVal = row[indexId];
if (match && match.length > 1) {
idVal = match[2];
p.indexByGroup[year].push(+idVal);
}
p.ids.push(idVal);
p.years.push(year);
return p;
},
{ indexByGroup: {}, ids: [], years: [] }
);
// Logger.log(JSON.stringify(id, null, ' '));
var newId = data
.map(function(row, i) {
if (row[indexId] !== '') return [row[indexId]];
if (isNumeric(id.years[i])) {
var lastId = Math.max.apply(
null,
id.indexByGroup[id.years[i]].filter(function(e) {
return isNumeric(e);
})
);
lastId = lastId === -Infinity ? 1 : lastId + 1;
id.indexByGroup[id.years[i]].push(lastId);
return [
Utilities.formatString(
'%s-%s',
id.years[i],
('000000000' + lastId).slice(-3)
)
];
}
return [''];
})
.slice(1);
sheet.getRange(2, indexId + 1, newId.length).setValues(newId);
}
I think it can be simplified in the feature.
There is an easier way to generate unique values that works for me, pick a #, then do +1. Ctrl C, then Ctrl shift V to paste back and remove the formula. Now you are left with thousands of unique IDs.
This is a manual solution but you can do an entire database in a matter of seconds every once in a while.
Related
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 =>
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('');
}
});
}
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:
Description: I have a sheet (Ppto) with a list of IDs (Id), Credits(Cedent) and Debits(Recept). I would like to move these transactions to MP05, If Id are equal. If Ids are not equal move to MP04. Criterion: If Id = use MP05. If Id ≠ use MP04.
I'm a novice in google script, I need some support.
Thanks for your attention
function mp() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ppto = ss.getSheetByName('Ppto.');
var Id = ppto.getRange('B5:B12').getValues();
var cedent = ppto.getRange('D5:D12').getValues();
var recept = ppto.getRange('E5:E12').getValues();
for (var i = 0; i < cedent.length; i++) {
for (var j = 0; j < recept.length; j++) {
if (cedent[i] != '' ) {
if (recept[j] != '' ) {
//if (Id === Id) // MP-05
//if (Id != Id) // MP-04
{
ppto.getRange('H5:H12').setValues(cedent);
ppto.getRange('I5:I12').setValues(recept);
Logger.log(cedent[i]);
ppto.getRange('j5:j12').setValues(cedent);
ppto.getRange('k5:k12').setValues(recept);
}
}
}
}
}
}
Strategy:
FIFO: First-In First Out
Loop through all rows using forEach
If credit is present, Loop again through all rows using some to look for receipts
If credit e[2] in first loop equals receipts f[3] in second loop, Check for id [0]
If ID is equal, splice two empty columns at the end, else at the 2nd position to create a uniform 6-column array
Set that array back to the sheet.
Sample Script:
function transactionSegregator() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ppto = ss.getSheetByName('Ppto.');
var data = ppto.getRange('B5:E12').getValues();
data.forEach(function(e) {
//e: Each row
if (e[2] && e.length == 4) {
//e[2]:credits; If this row is not spliced
data.some(function(f) {
//f:Each row; Second loop
if (e[2] == f[3]) {
//if credits = debit
if (e[0] == f[0]) {
//if id = id, splice two empty columns after Col4, else after Col2
e.splice(4, 0, '', '');
f.splice(4, 0, '', '');
} else {
e.splice(2, 0, '', '');
f.splice(2, 0, '', '');
}
return true;
}
});
}
});
Logger.log(data);
ppto.getRange(5, 6, data.length, data[0].length).setValues(data);// F5
}
References:
Javascript tutorial
Array#forEach
Array#some
Array#splice
I have a google spreadsheet that is populated by a form, so timestamps are automatically added in the first column for each row. I have a script that removes duplicate rows in my spreadsheet (5 specific columns must be the same for it to be a duplicate, while some other columns are ignored), but I want to modify it so that if I have multiple rows for the same person's data but with different timestamps, the script will keep the most recent row. How would I do this? Thanks!
/** removes duplicate rows in studentsheet **/
function removeDuplicates() {
var newData = new Array();
for(i in studentdata){
var row = studentdata[i];
var duplicate = false;
for(j in newData){
if(row[1] == newData[j][1] && row[2] == newData[j][2] && row[5] == newData[j][5] && row[9] == newData[j][9] && row[10] == newData[j][10]){
duplicate = true; //first name, last name, grade, dad's first name, and mom's first name are the same
}
}
if(!duplicate){
newData.push(row);
}
}
StudentSheet.clearContents();
StudentSheet.getRange(1, 1, newData.length, newData[0].length).setValues(newData);
sortSheet(); //sorts sheet by 2 columns
}
Here's a different approach, concattenating all columns in a single string, to save it as a object for faster searching, if you have a big sheet this can help:
function deleteDuplicateRowsSaveRecent(){
var verifiedRows = {},
curretnRow = "",
usedRows = [1, 2, 5, 9, 10];
for( lin in studentdata){
curretnRow = "";
for( ind in usedRows )
curretnRow += studentdata[ lin ][ usedRows[ ind ] ];
if(verifiedRows[ curretnRow ]){
if( studentdata[ lin ][ dateColumn ] > studentdata[ verifiedRows[ curretnRow ] ][ dateColumn ] ){
studentSheet.deleteRow(verifiedRows[ curretnRow ])
verifiedRows[ curretnRow ] = lin;
}else
studentSheet.deleteRow( lin );
}
else
verifiedRows[ curretnRow ] = lin;
}
}
Not tested but hopefully you'll get the logic.
Sorts data so grouped by 'test for duplicates' data and then by date descending within group,
Starts at bottom making bottom row current row.
Current row 'test for duplicates' tested against 'test for duplicates' in row above.
If current row duplicate of one above then deletes current row leaving the row above with the later date.
If not duplicate the row above becomes the current row and tested against the one above that deleting the current row if duplicate and moving on if not.
When complete replaces existing data in spreadsheet with modified data properly sorted.
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName("Form Responses 1");
// dataRange should not include headers
var dataRange = s.getRange(2, 1, s.getLastRow() -1, s.getLastColumn())
var data = dataRange.getValues();
// Test for duplicate columns.
// numbers below = column number; A=1 B=2 etc.
var lName = 2;
var fName = 3;
var grade = 5;
var dad = 9;
var mom = 10;
for( var i = 0; i < data.length; i++ ) {
// add sortable date to beginning of rows
data[i].unshift(Utilities.formatDate(data[i][0], "GMT", "yyyyMMddHHmmss"));
// add sortable test for duplicates string in front of above date.
// Placing the below in the order to be sorted by will save
// a separate sort later
data[i].unshift(
data[i][lName].toLowerCase().trim() +
data[i][fName].toLowerCase().trim() +
data[i][grade].toString().trim() +
data[i][dad].toLowerCase().trim() +
data[i][mom].toLowerCase().trim())
}
// sort to group rows by test data
data.sort();
// reverse sort so latest date at top of each duplicate group.
data.reverse();
// test each row with one above and delete if duplicate.
var len = data.length - 1;
for( var i = len; i > 0; i-- ) {
if(data[i][0] == data[i-1][0]) {
data.splice(i, 1);
}
}
// remove temp sort items from beginning of rows
for( var i = 0; i < data.length; i++ ) {
data[i].splice(0, 2);
}
// Current sort descending. Reverse for ascending
data.reverse();
s.getRange(2, 1, s.getLastRow(), s.getLastColumn()).clearContent();
s.getRange(2, 1, data.length, data[0].length).setValues(data);
}
After working up my previous answer, which I believe to be the better, I considered another approach that would cause less disruption to your existing code.
You push the first non duplicate from studentdata to the new array so if studentdata is sorted by timestamp descending before the test the first non duplicate encountered that is pushed will be the latest.
Placing the following at the very beginning of you function should achieve
for( var i = 0; i < studentdata.length; i++ ) {
// add sortable date to beginning of rows
studentdata[i].unshift(Utilities.formatDate(studentdata[i][0], "GMT", "yyyyMMddHHmmss"));
}
studentdata.sort();
studentdata.reverse();
// remove temp sort date from beginning of rows
for( var i = 0; i < studentdata.length; i++ ) {
studentdata[i].splice(0, 1);
}
I decided to sort the date of submission column so that the most recent date was on top, and then run my original duplicate removal script. It seemed to work.
/** sorts studentsheet by most recent submission, by last name, and then by grade/role (columns) **/
function sortSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Students");
sheet.sort(1, false); //sorts column A by date of submission with most recent on top
sheet.sort(3, true); // Sorts ascending (A-Z) by column C, last name
sheet.sort(6, true); // Sorts ascending (A-Z) by column F, grade/role
}
function removeDuplicates(){
var newData = new Array();
for(i in studentdata){
var row = studentdata[i];
var duplicate = false;
for(j in newData){
if(row[1] == newData[j][1] && row[2] == newData[j][2] && row[5] == newData[j][5] && row[9] == newData[j][9] && row[10] == newData[j][10]){
duplicate = true; //date of submission, first name, last name, grade, dad's first name, and mom's first name are the same
}
}
if(!duplicate){
newData.push(row);
}
}
StudentSheet.clearContents();
StudentSheet.getRange(1, 1, newData.length, newData[0].length).setValues(newData);
}