Best approach to group people into teams using Google Apps Script - javascript

Essentially there are around ~200 in this department. We are looking to create squads of 4, where each squad must be from a different team, located in the same city, one must be a driver (and the other 3 therefore should not be drivers), and all four people on the team should have similar preferences. These criteria are captured via a Google Form so ultimately we will get the relevant data we need to build out the conditions. My only challenge currently is that I am not well-versed in Apps Script so will need high level guidance on how to approach it.
Currently, I am thinking of storing each column in their own respective arrays and then using a For loop and conditional statements within that to try to satisfy all the requirements. For example, the first conditional statement would be whether the person drives, and if they do then put them into this new array (which will be defined). Next, check the next response and if that person doesnt drive, and has same same preferences, is located in same city, and is on a different team, then include them into that array. Otherwise, move onto the next person and check for those requirements. Once the group hits 4 people, set the values into the Google Sheet somewhere and then search for the next driver in the list and build another group using same logic.
Alternative approach might be to first sort people into 2 groups (based on the two cities), then within each group sort folks based on driver and add in the other criterias as well.
Regardless of which approach I take in terms of algorithm, would first storing each column into an array make the most sense, then managing the array values using various conditional statements.
Here is what I have so far:
function matchUsers() {
//store each column into arrays for use later on
var responseSheet = SpreadsheetApp.getActive().getSheetByName("Responses");
var Alast = responseSheet.getRange("A2:A").getValues().filter(String).length;
Logger.log(Alast);
var emails = responseSheet.getRange("A2:A").getValues();
var userEmails = responseSheet.getRange(2,1, Alast,1).getValues();
var tmName = responseSheet.getRange(2,2, Alast,1).getValues();
var team = responseSheet.getRange(2,3, Alast,1).getValues();
var city = responseSheet.getRange(2,4, Alast,1).getValues();
var firstChoice = responseSheet.getRange(2,5, Alast,1).getValues();
var secChoice = responseSheet.getRange(2,6, Alast,1).getValues();
var thirdChoice = responseSheet.getRange(2,7, Alast,1).getValues();
var driveStatus = responseSheet.getRange(2,8, Alast,1).getValues();
var arrayResponses = [userEmails,tmName,team,city,firstChoice,secChoice,thirdChoice, driveStatus];
//clear sheets
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Output").getRange(2, 1,20,50).clearContent();
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("CitySort").getRange(2, 1,20,50).clearContent();
//prepare arrays
var edmTMs = [];
var calTMs = [];
var edmDrivers = [];
var calDrivers = [];
//loop through each row and sort team members by respective city and driving capability
for (let i = 0; i < Alast;i+=1){
if (arrayResponses[3][i] == "Edmonton" && arrayResponses[7][i] == "Yes"){
edmDrivers.push(arrayResponses[1][i]);
}else if (arrayResponses[3][i] == "Edmonton" && arrayResponses[7][i] == "No"){
edmTMs.push(arrayResponses[1][i]);
}else if (arrayResponses[3][i] == "Calgary" && arrayResponses[7][i] == "Yes"){
calDrivers.push(arrayResponses[1][i]);
}else if (arrayResponses[3][i] == "Calgary" && arrayResponses[7][i] == "No"){
calTMs.push(arrayResponses[1][i]);
}
}
Logger.log("\nEdmonton: "+edmTMs + "\nCalgary: "+calTMs );
var range = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("CitySort").getRange(2,1,edmTMs.length,1);
range.setValues(edmTMs);
var range = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("CitySort").getRange(2,2,calTMs.length,1);
range.setValues(calTMs);
var range = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("CitySort").getRange(2,3,edmDrivers.length,1);
range.setValues(edmDrivers);
var range = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("CitySort").getRange(2,4,calDrivers.length,1);
range.setValues(calDrivers);
}

Let's see if this fits your needs. I'm pretty sure there are more elegant and efficient solutions than this one, so I appreciate any suggestion.
I tested the script with 34 users, and I got 8 teams of 4 and 2 teams of 1:
Don't mind the repeated names, they are all different rows with different Cities, Teams and Choices. I added a letter to some of them to differentiate.
The script works as follows:
It gets each user row and compares the City, Teams and Choices with all the other 33. It also discards the users that are already in a team. The priority is City > Team > Choice 1 > Choice 2 > Choice 3.
It will also try to force teams of 4 members, so in case there is a team of 3, it will ignore the Teams to try to find the 4th, but it still will respect the Choices and Cities. You can disable this by setting the boolean ignoreTeams to false. In my test, only 3 groups have 1 repeated team.
The script will never put two users from different cities in the same group.
function main() {
var sprsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet_1 = sprsheet.getSheetByName("Members");
var sheet_2 = sprsheet.getSheetByName("Output");
var number = sheet_1.getLastRow() - 1;
var members = []; //Each row of Members will be stored here
getMembers(sheet_1, members, number);
orderMembers(sheet_2, members);
}
function getMembers(sheet_1, members, number){
for (var i = 0; i < number; i++){
var values = sheet_1.getRange(i+2, 1, 1, 8).getValues();
members.push(new Person(values[0][0],values[0][1],values[0][2],values[0][3],values[0][4],values[0][5],values[0][6],values[0][7]));
}
}
function orderMembers(sheet_2, members){
var col = 1;
var counter = 0; //Number of Persons in each group column [0-3]
loop1:
for (var i = 0; i < members.length; i++){
var loopbreaker = 0; //Avoids an endless loop if ignoreTeams = true
var ignoreTeams = false;
var row = 2;
if (!members[i].assigned || i == 0){
var teams = []; //Array of teams of each group
sheet_2.getRange(row, col).setValue(members[i].name);
members[i].assigned = true;
teams.push(members[i].team);
loop2:
for (var loop = 0; loop < 9; loop++){
var value = order_params(i, loop, members, teams, ignoreTeams);
if (value != null){
sheet_2.getRange(row + 1, col).setValue(value); //Writes in sheet Output
counter++;
row++;
}
//If the group is smaller than 4 and the loop is over, it will ignore the teams and start again in order to complete the group.
if (counter < 3 && loop == 8 && loopbreaker < 1){
loop = 0;
ignoreTeams = true; //Set to false to never ignore teams
loopbreaker++;//The loopbreaker avoids an endless loop
} else if (counter == 3 || loop == 8){
col++;
counter = 0;
break loop2;
}
}
}
}
}
function order_params(i, loop, members, teams, ignoreTeams){
for (var x = 0; x < members.length; x++){
if (!members[x].assigned && members[x].email != members[i].email && members[x].city == members[i].city && (!checkTeams(members[x].team, teams) || ignoreTeams)) {
if (members[x].choice1 == members[i].choice1 && loop < 3) {
members[x].assigned = true;
teams.push(members[x].team);
return members[x].name
} else if (members[x].choice2 == members[i].choice1 && loop >= 3 && loop < 6) {
members[x].assigned = true;
teams.push(members[x].team);
return members[x].name
} else if (members[x].choice3 == members[i].choice1 && loop >= 6 && loop < 9) {
members[x].assigned = true;
teams.push(members[x].team);
return members[x].name
}
}
}
}
//Checks for duplicate teams in the group
function checkTeams(member_t, teams){
for (var i = 0; i < teams.length; i++){
if (member_t == teams[i]){
return true;
}
}
}
//Converts the data from the Sheet into Person objects.
function Person(email, name, team, city, choice1, choice2, choice3, driver){
this.email = email;
this.name = name;
this.team = team;
this.city = city;
this.choice1 = choice1;
this.choice2 = choice2;
this.choice3 = choice3;
this.driver = driver;
this.assigned = false; //Assigned = true means the Person is already in a group
}
As you see I'm not using the field Driver, so you can make a condition in case you see too many groups without drivers, or change the members manually. If this is a serious problem, I will modify the code.

Related

Log repeated values in 2d array

I'm trying to optimise a whole school timetable. I have the timetable currently organised in a sheet. Teacher initials are the headings for each column and each row corresponds to a single teaching period in a 30 lesson week. Each cell contains the name of a class.
Currently I am looking for classes that are split between 2 teachers.
I am trying to make an appscript that will log the classname if it appears anywhere outside the current column (i.e. the same class is being taught by 2 or more different teachers at different times)
I'm aware that nesting loops is not the least efficient way of doing this but I just wanted to hack something together quickly to get the job done. Unfortunately this code is taking longer than the maximum permitted time. The array is only 30 rows by about 56 columns so I dont see why it's taking such a long time. (Cant see anything that's obviously infinite about my loops either)
Can anyone help? :)
function splitClassLocator()
{
//copy the sheet to a 2d array.
//(1)descend through each column from vertical idx 3 to period6 idx36
//(2)start at horiz idx 1, descend through each item vertically.
//if item from loop 1 matches item from loop 2 and loop 1 vertical index != loop 2 vertical index
//log the item (split class)
//GET THE DATA
var sh0 = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var range = sh0.getDataRange();
var data = range.getValues();
//COMPARE
//main row mr, main col mc V compare row cr, compare col cc
Logger.log("Rows = " + data.length + "cols = " + data[0].length);
for (var mr = 2; mr < data.length; mr ++)
{
for (var mc = 1; mc < data[0].length; mc ++)
{
for (var cr = 2; cr < data.length; cr ++)
{
for (var cc = 1; cc, data[0].length; cc ++)
{
if (mc != cc) // if it's not comparing classes belonging to the same teacher
{
if ((data[mr][mc] != undefined) || (data[mr][mc] != null) || (data[mr][mc] != ""))
{
if (data[mr][mc] == data[cr][cc])
{
Logger.log(data[mr][mc]);
}
}
}
}
}
}
}
}
Rather than discard the information after your comparison, store it and use it! Welcome to the world of not-Arrays!
Your stated goal is to find classes that have more than two teachers. Thus, the minimum you need to do is tally this information on a single trip through the array.
function countTeachersPerClass() {
const schedule = SpreadsheetApp.getActive().getSheetByName("somename").getDataRange().getValues();
const headers = schedule.shift();
const numTeachers = headers.length;
// Store classes as properties in an Object.
const classes = {};
// Read the schedule array.
// Assumption: time is in index 0, classes in all other.
schedule.forEach(function (period) {
var name = period[0];
// Store each class in this period.
for (var teacherIndex = 1; teacherIndex < numTeachers; ++teacherIndex) {
// Have we seen this class? If no, initialize it.
var classID = period[teacherIndex];
if (!classes[classID])
classes[classID] = {teachers: {} };
// Add this teacher to its list of teachers.
var tID = headers[teacherIndex];
if (!classes[classID].teachers[tID])
classes[classID].teachers[tID] = {periods: []};
// Add this period for this teacher.
classes[classID].teachers[tID].periods.push(name);
} //End for loop over columns in a row.
}); // End forEach over schedule's rows.
// Log the object (in Stackdriver, for easy review and interactivity).
console.log({message: "Built classes object", input: schedule, result: classes, teachers: headers});
// Make a report from it.
const report = [["Class ID", /* other headers */]];
for (var cID in classes) {
// This particular report is for class with more than two teachers.
if (Object.keys(classes[cID].teachers).length > 2) {
var row = [cID];
/** Push info to the row, perhaps with just the names
of the teachers, or also including number of
periods per each teacher, etc. */;
// Add the completed report row to the report.
report.push(row);
}
}
// Log the report.
console.info({message: "Report", report: report});
}
You could certainly get fancier by adding more properties to each classes object than just teachers, such as tracking the average consecutive taught time (i.e. which classes alternate teachers often), but I leave that as an exercise to the reader :)
This works a bit better, flattening to make 2 single dimensional arrays, one is the teacher to check and the other array is all the other timetables combined.
Still takes ages though:
function splitClasses()
{
var sh0 = SpreadsheetApp.getActiveSpreadsheet().getSheets()[1];
var range = sh0.getDataRange();
var data = range.getValues();
var rows = data.length;
var cols = data[0].length;
for (var col = 1; col < cols; col ++)
{
var teacherTT = [];
var othersTT = [];
for (var row = 2; row < rows; row ++)
{
teacherTT.push(data[row][col]);
}
for (var oCol = 1; oCol < cols; oCol ++)
{
for (var oRow = 2; oRow < rows; oRow ++)
{
if (col !=oCol)//dont add current teacher TT to others TT
{
othersTT.push(data[oRow][oCol]);
}
}
}
//Logger.log(othersTT);
var tLength = teacherTT.length;
var oLength = othersTT.length;
Logger.log("tL" + tLength);
Logger.log("oL" + oLength);
for (var t = 0; t < tLength; t ++)
{
//Logger.log("t "+t);
for (var o = 0; o < oLength; o ++)
{
if (teacherTT[t] != undefined ||teacherTT[t] != null || teacherTT[t] != "" || teacherTT[t] != " ")
{
if (teacherTT[t])
{
if (teacherTT[t] == othersTT[o])
{
//Logger.log("o "+o);
Logger.log(teacherTT[t]);
}
}
}
}
}
}
}

Using for and case loops to sortand move between google sheets and Spreadsheets

below is some script I have tride to piece together as best I can at this point. But I am legally blind and need to get some automation done (Hence this script), and when I put into google script editor, it keeps throwing errors "missing ; before line..." but I have checked, and double checked, and from what I can see all ";" that should be in it are ;(!
.
As a VBA coder, I COULD do most of this via that, but have opted for Google for Sheets and forms, so need to get a grasp on this darn JavaScript stuff now.
Can someone please take some time to look over this, and outline where the errors are?
Much of this code has come from bits and pieces here on stack overflow to see if I can nut it out and learn myself, but I really need some help on this one right now (in a way that will let me learn at the same time).
The concept of this script is eventually to take form submissions and sort them into other sheets on the Spreadsheet based on "courseChoices" and then further on via "Locations"
function change(e) { var ss = SpreadsheetApp.getActiveSpreadsheet();
`` var sourceSheet = ss.getSheetByName('Form Responses 1');
var sourceRows = sourceSheet.getDataRange();
var numSourceRows = sourceRows.getNumRows();
var sourceRowValues = sourceRows.getValues();
var rowsTransferred = 0;
for (var i = 0; i <= numSourceRows - 1; i++) {
var thisSourceRowValue = sourceValues[i];
// See if the course is a multiple choice of courses
// in this case column 8 (array 7)
if (thisSourceRowValue[7].indexOf(",") < 0) {
// Is not Multiple choice
Var CourseChoices = thisSourceRowValue[7]
}
else{
var courseChoices = thisSourceRowValue[7].split(",");
for(var x = 0; x <= thisSourceRowValue[7].indexOf(","); x++)
switch(courseChoices(x)) {
case courseChoices(x).indexOf("Dual") > -1;
case courseChoices(x).indexOf("Childhood") > -1;
var Target = "Dual Diploma";
var courseName = "Dual Diploma in Early Childhood Education and Care";
break;
case courseChoices(x).indexOf("Sector") > -1;
var Target = "Adv Sector";
var courseName = "Advanced Diploma in Community Sector Management";
break;
case courseChoices(x).indexOf("Adv") > -1;
var Target = "Adv Management";
var courseName = "Advanced Diploma in Leadership and Management";
break;
case courseChoices(x).indexOf("Business") > -1;
var Target = "CertIII Business";
var courseName = "Certificcate III in Business";
break;
case Default;
var Target = "Other";
var courseName = courseChoices(x);
}
// checked all multiple choices
}
// Made sure we have ALL Choices now
}
// See if the sheet exists do more stuff
if (!ss.getSheetByName(Target)) {
// Tab does not exist, so create it
ss.insertSheet(Target);
// Ensured Target Tab Exists
// Insert first row as headers
// sort order of columns ....
var range_input = sourceSheet.getRange("A1:N1");
var range_output = Target.getRange("A1");
//sort columns into new orders
var keep_columns = [0,1,2,3,4,5,10,9,8,11,6,7, 12];
//Now copy the columns into the target in the new order
copyColumnNumbers(range_input, range_output, keep_columns);
}
// set our target sheet and target range
var colNumber = sourceSheet.getLastColumn()-1;
var targetSheet = ss.getSheetByName(Target);
var targetRange = targetSheet.getRange(targetSheet.getLastRow()+1, 1, 1, colNumber);
// get our source range/row
var sourceRange = SourceSheet.getRange(rowIndex, 1, 1, colNumber);
var targetRows = targetSheet.getDataRange();
var numTargetRows = targetRows.getNumRows();
var targetValues = targetRows.getValues();
for (var y = 0; y <= numTargetRows - 1; y++) {
var thisTargetRow = targetValues[y];
// See if the first name, last name and Email are the same
if (thisTargetRow[2] == thisSourceRow[2] && thisTargetRow[3] == thisSourceRow[3] && thisTargetRow[5] == thisSourceRow[5]) {
// Record Exists, so just alter the coursename
thisTargetRow[7] == courseName;
break;
}Else if (targetRow[0] == ""){
// WE have come to a blank line in the target sheet
// Alter Course to proper Name
thisTargetRow[7] == courseName;
// Add new record in correct column orders
var range_input = thisTargetRow;
var range_output = thisTargetRow
//sort columns into new orders
var keep_columns = [0,1,2,3,4,5,10,9,8,11,6,7, 12];
//Now copy the columns into the target in the new order
copyColumnNumbers(range_input, range_output, keep_columns);
break;
}
// records checked in target
}
// We have checked all the records now in the Source Sheet
}
// ALL DONE!
}
I do have a habit of commenting extensively in my coding, so hope that does not put you off. Because I cannot see the start and finish of a line alwyys (And therefore star and end of blocks of code) I use comments to state what I am dong, and wher
please email me some fixes if ossible, and WHERE my mmistakes are (As emails will be MUCH easier for me to have read aloud by my voice programs.
Thanks IN advance all ;)

Javascript convert array of numbers with duplicates into unique sets of numbers

I have a javascript sorting algorithm problem which I am struggling to solve efficiently .
I have an array of numbers which I am calling cards e.g. [1,2,3,4,5,6,7,8,9,10]
Each day I have 6 teaching sets. In each set I want to display a maximum of 5 RANDOM & UNIQUE cards. Each card must be displayed exactly 3 times a day.
What I have done so far is to create 6 empty arrays (sets). I then iterate through my cards 3 times each attempting to add them to a random set if the card does not already exist in that array. Sometimes it works, but most of the time I get a problem where I have only one array with space left and that array already contains the card. my code:
Assume in my code that numberOfSetsPerCard = 3 & totalSets = 6. The only library available is JQuery.
shuffleIntoSets : function(cards, numberOfSetsPerCard, totalSets) {
var sets = [];
// initialize the sets as empty arrays
for (i = 0; i < totalSets; i++) {
sets[i] = [];
}
$.each(cards, function(index, card) {
for(x=0;x<numberOfSetsPerCard;) {
// attempt to place the card in a set which doesnt already contain the card
setNo = Math.floor((Math.random() * totalSets));
console.log("setNo: " + setNo);
if(jQuery.inArray(card,sets[setNo]) == -1 && sets[setNo].length<5) {
console.log(setNo + "does not contain: " + card);
sets[setNo].push(card);
console.log("Added the card, set now looks like :" + sets[setNo]);
x++;
}
}
});
return sets;
},
This is my solution to your problem, it is a bit long, but i think it a bit long, but it definitely creates randomness in the teach sets, and satisfy the conditions you have stated
codepen link: http://codepen.io/anon/pen/yyoaZy
html/include jquery:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
js:
//-----------------------------------------------
//For this question on StackOverflow:
//http://stackoverflow.com/questions/28192127/javascript-convert-array-of-numbers-with-duplicates-into-unique-sets-of-numbers
//-----------------------------------------------
var cards = [1,2,3,4,5,6,7,8,9,10];
var numOfTeachingSets = 6;
var numOfUniqueCardsPerTeachingSet = 5;
var numOfCardRepetitionPerDay = 3;
var shuffleCards = function(cards, numOfTeachingSets, numOfUniqueCardsPerTeachingSet, numOfRepetitionPerCard){
if(cards.length*numOfRepetitionPerCard != numOfTeachingSets*numOfUniqueCardsPerTeachingSet){
alert("invalid param");
return null;
}
//since each card is required to repeat numOfRepetitionPerCard times
//the available choices should be numOfRepetitionPerCard times of the original deck of cards
var availableCardChoices = cards.concat([]);
for (var i=0;i<numOfRepetitionPerCard-1;i++){
availableCardChoices = availableCardChoices.concat(cards);
}
//Record down items from [availableCardChoices] has been picked
var choosenList = [];
//Put 6 sets of unique cards into the result
var result = [];
for (var i=0;i<numOfTeachingSets;i++){
result.push( pickOutUniqueNumberSet(availableCardChoices,numOfUniqueCardsPerTeachingSet, choosenList) );
}
//return the result - an array of [numOfTeachingSets] arrays
return result;
}
//-----------------------------------------------------------
// Function:
// picks [cardsPerSet] number of unique item out of [availableChoices]
// each time an item is picked, this item is removed from [availableChoices]
//
// Important note: The number of card repetition is not really meaningful
// because if each round of picking produces unique sets,
// then the number of card repetition condition will be
// automatically satisfied.
//-----------------------------------------------------------
var pickOutUniqueNumberSet = function(availableChoices, cardsPerSet, disabledChoices){
if (cardsPerSet==0 || availableChoices.length==0){
return null;
}
var choosenSet = [];
var maxAttempts = 10000; //these 2 counter are used to prevent infinite looping
var attempts = 0;
for(var i=0;i<cardsPerSet;i++){
//items are choosen from [availableChoices] by their array index.
var randomIndex = Math.floor((Math.random() * availableChoices.length));
//repeatedly grab a random index from availableChoices till a unique one is selected
//unique item is an item that is not repeated in choosenSet, and its index is not in disabledChoices
while( (InArray(choosenSet, availableChoices[randomIndex]) || InArray(disabledChoices, randomIndex)) && attempts<maxAttempts){
randomIndex = Math.floor((Math.random() * availableChoices.length));
attempts++;
}
//Add this item to the chooseSet
choosenSet.push(availableChoices[randomIndex]);
//Add this item's index to disabledChoices
disabledChoices.push(randomIndex);
}
return choosenSet;
}
var InArray = function(array, itemToFind){
for(var i=0;i<array.length; i++){
if(array[i]==itemToFind){
return true;
}
}
return false;
}
//---------- Test --------
var teachingSets = shuffleCards(cards, numOfTeachingSets, numOfUniqueCardsPerTeachingSet, numOfCardRepetitionPerDay);
for(var i=0;i<teachingSets.length; i++){
document.write(teachingSets[i] + "<br>");
}

Google sheets: How do I get my custom function to refresh automatically?

I have written a Google Sheet application for myself that (among other things) has a schedule for consultants to work on projects. I have written a custom function that displays summary information from the calendar; for example:
Tubb, Ernest|Project 1
Tubb, Ernest|Project 2
Perkins, Carl|Project 1
I wrote the function because the mess of FILTER() functions I was using was incomprehensible; and the javascript required to do the same thing is relatively straightforward.
What I find however, is that when the underlying data changes, the cells written by my function are NOT recalculated the way they were with the FILTER() function. How can I program some sort of listener that makes my function's output be refreshed when the underlying data changes? Do I have to do this by hand with the onEdit() function?
And here is my function:
// Global Constants - these will change each quarter based on the calendar structure.
var CALENDAR_QUARTER_START = "F"
var CALENDAR_QUARTER_END = "CS"
// Generate a nice 2-column display of Consultant and Project(s) they are scheduled on,
// based on the consultant names and calendar codes in the Calendar tab.
// "billable" parameter specifies the return of only uppercase activities (true) or all activities (false)
function fortifyConsultantsAndProjects( billable ){
// Resolve arguments
var billable = arguments[0];
if (arguments[0] == undefined) billable = true;
var calendar = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Calendar");
var consultants = calendar.getRange( "A8:A" ).getValues();
var ret = [];
var row = 0;
for ( var i=0; i<consultants.length; i++ ){
var consultant = consultants[i].toString();
var projects = fortifyGetProjectsForConsultant( consultant, billable );
for (var j=0; j < projects.length; j++ ) {
ret.push( [] );
ret[row][0] = consultant;
ret[row][1] = projects[j];
row++;
}
}
return ret;
}
function fortifyGetProjectsForConsultant( consultant, billable ){
// Resolve arguments
var consultant = arguments[0];
if (arguments[0] == undefined) consultant = "Held, Doug";
var billable = arguments[1];
if (arguments[1] == undefined) billable = true;
// Get the range of consultants defined in Column A of Calendar tab.
var calendar = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Calendar");
var consultants = calendar.getRange( "A8:A" ).getValues();
// Identify which Calendar row contains the specified consultant's schedule.
var row;
for ( var i=0; i< consultants.length; i++){
//Browser.msgBox( "compare " + consultant + " and " + consultants[i] );
if ( consultants[i].toString() == consultant ){
row = i+8;
break;
}
}
// Obtain all the consultant's schedulings. Contains many duplicates, so sort.
var projects = calendar.getRange( CALENDAR_QUARTER_START + row + ":" + CALENDAR_QUARTER_END + row + "" ).getValues()[0].sort();
// Iterate through the sorted project codes, removing duplicates and blanks
var ret = [];
var row = 0;
var prev = "";
for ( var i = 0; i< projects.length; i++ ){
var temp = projects[i].toString();
if (temp != "" && temp != prev ){
// Resolve whether to return each row based on project billability (uppercase)
if ( temp.toUpperCase() == temp || billable == false ){
ret[row] = temp;
row++;
}
prev = temp;
}
}
return ret;
}
Well since nobody ever answered, I will explain how I solved the problem.
Instead of a zero-argument function parameter list, I receive all of the ranges (for instance the range Calendar!A8:A that you see hard coded here). Then any time data in the parameter ranges changes, the function is invoked again. This does occasionally cause some sort of runtime error in Google sheets - on update, I sometimes get "the document is not available" and an instruction to reload the sheet.
When a range is received as a parameter, that data is received byvalue as a 2 dimensional array of cell values -- the same as the array returned from Range.getValues().

How can I remove rows with unique values, keeping rows with duplicate values?

I have a spreadsheet of surveys, in which I need to see how particular users have varied over time. As such, I need to disregard all rows with unique values in a particular column. The data looks like this:
Response Date Response_ID Account_ID Q.1
10/20/2011 12:03:43 PM 23655956 1168161 8
10/20/2011 03:52:57 PM 23660161 1168152 0
10/21/2011 10:55:54 AM 23672903 1166121 7
10/23/2011 04:28:16 PM 23694471 1144756 9
10/25/2011 06:30:52 AM 23732674 1167449 7
10/25/2011 07:52:28 AM 23734597 1087618 5
I've found a way to do so in Excel VBA:
Sub Del_Unique()
Application.ScreenUpdating = False
Columns("B:B").Insert Shift:=xlToRight
Columns("A:A").Copy Destination:=Columns("B:B")
i = Application.CountIf(Range("A:A"), "<>") + 50
If i > 65536 Then i = 65536
Do
If Application.CountIf(Range("B:B"), Range("A" & i)) = 1 Then
Rows(i).Delete
End If
i = i - 1
Loop Until i = 0
Columns("B:B").Delete
Application.ScreenUpdating = True
End Sub
I'd like to do it in Google Spreadsheets with a script that won't have to be changed. Closest I can get is retrieving all duplicate user ids from the range, but can't associate that with the row. That code follows:
function findDuplicatesInSelection() {
var activeRange = SpreadsheetApp.getActiveRange();
var values = activeRange.getValues();
// values that appear at least once
var once = {};
// values that appear at least twice
var twice = {};
// values that appear at least twice, stored in a pretty fashion!
var final = [];
for (var i = 0; i < values.length; i++) {
var inner = values[i];
for (var j = 0; j < inner.length; j++) {
var cell = inner[j];
if (cell == "") continue;
if (once.hasOwnProperty(cell)) {
if (!twice.hasOwnProperty(cell)) {
final.push(cell);
}
twice[cell] = 1;
} else {
once[cell] = 1;
}
}
}
if (final.length == 0) {
Browser.msgBox("No duplicates found");
} else {
Browser.msgBox("Duplicates are: " + final);
}
}
This is maybe not very efficient, but I think it's what you want:
var ar=[1,3,3,5,6,8,6,6];
console.log("Before:");
display(ar);//1 3 3 5 6 8 6 6
var index=[];
var ar2=[];
for(var a=0;a<ar.length;a++)
{
var duplicate=false;
for(var b=0;b<ar.length;b++)
{
if(ar[a]==ar[b]&&a!=b)
{
duplicate=true;
}
}
if(!duplicate)
{
index.push(a);
}
}
for(var a=0;a<index.length;a++)
{
ar[index[a]]=null;
}
for(var a=0;a<ar.length;a++)
{
if(ar[a]!=null)ar2.push(ar[a]);
}
console.log("After:");
display(ar2);//3 3 6 6 6
function display(x)
{
for(var a=0;a<x.length;a++)console.log(x[a]);
}
The fiddle : http://jsfiddle.net/mageek/6AGQ4/
And a shorter version that is as a function :
var ar=[1,3,3,5,6,8,6,6];
function removeUnique(x)
{
var index=[];
var ar2=[];
for(var a=0;a<ar.length;a++)
{
var duplicate=0;
for(var b=0;b<ar.length;b++)if(ar[a]==ar[b]&&a!=b)duplicate=1;
if(!duplicate)index.push(a);
}
for(var a=0;a<index.length;a++)ar[index[a]]=null;
for(var a=0;a<ar.length;a++)if(ar[a]!=null)ar2.push(ar[a]);
return x;
}
ar=removeUnique(ar);
The fiddle : http://jsfiddle.net/mageek/6AGQ4/2
I'd suggest going for something simple.
Create a short script that flags duplicates
Write the formula directly into the cell "=flagDuplicate(C2,C$2:C$10)"
Copy the forumla down the column
Use Spreadsheet's built in QUERY formula to pull the information you need
"=QUERY(A1:E10; "SELECT * WHERE E = TRUE"; 1)"
Here is a simple function to flag duplicates
function flagDuplicate(value, array) {
var duplicateCounter = 0;
for (var i=0; i<array.length; i++){
if (array[i] == value){ // I avoid === in Spreadsheet functions
duplicateCounter++;
}
}
if (duplicateCounter > 1){
return true;
}else{
return false;
}
}
Too many functions on a large table can slow things down. If it becomes a problem, you can always copy and "paste values only" - that will retain the information but remove the functions.
Best of luck.
Note: When I tested this I noticed that can take a while before the spreadsheet recognizes the new custom function (gives error like can't find function FLAGDUPLICATE)
You could also do it using arrays to handle the whole sheet at once :
function removeUnique(){
var col = 2 ; // choose the column you want to check for unique elements
var sh = SpreadsheetApp.getActiveSheet();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var data=ss.getDataRange().getValues();// get all data
data.sort(function(x,y){
// var xp = Number(x[col]);// use these to sort on numeric values
// var yp = Number(y[col]);
var xp = x[col];// use these for non-numeric values
var yp = y[col];
Logger.log(xp+' '+yp); // just to check the sort is OK
return xp == yp ? 0 : xp < yp ? -1 : 1;// sort on column col numeric ascending
});
var cc=0;
var newdata = new Array();
for(nn=0;nn<data.length-1;++nn){
if(data[nn+1][col]==data[nn][col]||cc>0){
newdata.push(data[nn]);
++cc;
if(cc>1){cc=0}}
}
ss.getDataRange().clearContent(); // clear the sheet
sh.getRange(1,1,newdata.length,newdata[0].length).setValues(newdata);// paste new values sorted and without unique elements
}
EDIT : here is the version that keeps all duplicates (the working one)
function removeUnique(){
var col = 2 ; // choose the column you want to check for unique elements
var sh = SpreadsheetApp.getActiveSheet();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var data=ss.getDataRange().getValues();// get all data
data.sort(function(x,y){
// var xp = Number(x[col]);// use these to sort on numeric values
// var yp = Number(y[col]);
var xp = x[col];// use these for non-numeric values
var yp = y[col];
Logger.log(xp+' '+yp); // just to check the sort is OK
return xp == yp ? 0 : xp < yp ? -1 : 1;// sort on column col numeric ascending
});
var newdata = new Array();
for(nn=0;nn<data.length-1;++nn){
if(data[nn+1][col]==data[nn][col]){
newdata.push(data[nn]);
}
}
if(data[nn-1][col]==data[nn][col]){newdata.push(data[nn])}
ss.getDataRange().clearContent(); // clear the sheet
sh.getRange(1,1,newdata.length,newdata[0].length).setValues(newdata);// paste new values sorted and without unique elements
}

Categories

Resources