How to append data to end of rows in Google Apps Script - javascript

One of our high schools is trying to create weekly reports for students who are currently failing one or more classes. The reports are in the format linked below.
If a student is failing multiple classes, the information is on different rows. The information needs to be combined, with each student on one column - column 1 is the name, and the subsequent columns are for the grade/class/teacher information.
Currently, I have code that deletes blank sheets, creates a new sheet titled "Output", writes a header row, and writes unique names in column 1 (portions commented out so it doesn't create a duplicate "Output" sheet every time):
function copyRows() {
//Initialize variables
var ss = SpreadsheetApp.getActiveSpreadsheet();
var dataSheet = ss.getSheetByName("Sheet1");
/*
//Create output Sheet
ss.insertSheet("Output");
*/
var writeSheet = ss.getSheetByName("Output");
/*
//Delete unwanted columns (1, 3, 4, 7, 9)
dataSheet.deleteColumn(1); //Deletes first column
dataSheet.deleteColumn(2); // Deletes third column (because it shifts after every deletion)
dataSheet.deleteColumn(2); //Deletes fourth column
dataSheet.deleteColumn(4); //Deletes 7th column
dataSheet.deleteColumn(5);//Deletes 9th column
//Delete unwanted Sheets
var deleteSheet2 = ss.getSheetByName("Sheet2");
var deleteSheet3 = ss.getSheetByName("Sheet3");
ss.deleteSheet(deleteSheet2);
ss.deleteSheet(deleteSheet3);
//Write data to header row
var headerRange = writeSheet.getRange("A1:V1");
var headerValues = [
["name", "class1", "grade1", "teacher1","class2", "grade2", "teacher2","class3", "grade3", "teacher3","class4", "grade4", "teacher4","class5", "grade5", "teacher5","class6", "grade6", "teacher6","class7", "grade7", "teacher7"]
]
headerRange.setValues(headerValues);
*/
var lastRow = dataSheet.getLastRow();
var mainArray = dataSheet.getRange(1, 1, lastRow, 4).getValues();
var allNames = []; //List of all names, including duplicates
var uniqueNames = []; //List of all unique names
for (i = 0; i < mainArray.length; i++) { //Sets allNames
allNames.push(mainArray[i][0]);
}
for (i = 0; i < allNames.length; i++) { //Sets uniqueNames
if (allNames[i+1] != allNames[i]) {
uniqueNames.push(allNames[i]);
}
}
var uniqueNamesArray = uniqueNames;
//New method that converts 1d array to 2d array
Array.prototype.reshape = function(rows, cols) {
var copy = this.slice(0); // Copy all elements.
this.length = 0; // Clear out existing array.
for (var r = 0; r < rows; r++) {
var row = [];
for (var c = 0; c < cols; c++) {
var i = r * cols + c;
if (i < copy.length) {
row.push(copy[i]);
}
}
this.push(row);
}
};
var uniqueNamesRow = uniqueNames;
uniqueNames.reshape(uniqueNames.length, 1); //Changing uniqueNames from row to column
var writeNamesRange = writeSheet.getRange(2,1,uniqueNames.length,1); //writeSheet column 1
writeNamesRange.setValues(uniqueNames);
Example data:
John Doe 50 Band Mr. Dean
Mary Smith 60 US History Ms. Jones
Mary Smith 25 Chemistry Ms. Dyar
Mary Smith 40 Algebra 2 Ms. Harris
Bob Miller 55 Band Mr. Dean
Larry Jones 22 Algebra 2 Ms. Harris
With the output of:
John Doe 50 Band Mr. Dean
Mary Smith 60 US History Ms. Jones 25 Chemistry Ms. Dyar 40 Algebra 2 Ms. Harris
Bob Miller 55 Band Mr. Dean
Larry Jones 22 Algebra 2 Ms. Harris
Note that Mary Smith's data has been combined into one row.
I just cannot figure out how to iterate through the rows and append the data to the end of the appropriate row.
Sorry for the previous lack of details, and thanks for the feedback.

I wrote some codes based on your input and output (not on your code). Since student's name is a primary key, I think dictionary data structure is suitable here. Below code is not so elegant and many literals were hard-coded but I guess you'll be able to grasp the idea. Sample sheet is here.
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('input');
var rows = 6;
var cols = 4;
var input = ss.getRange(1,1, rows,cols).getValues();
var students = [];
for(var i = 0; i < input.length; i++) {
var name = input[i][0];
var class = {
score: input[i][1],
class: input[i][2],
teacher: input[i][3]
};
var j;
for(j = 0; j < students.length; j++) {
if(students[j].name == name) {
students[j].classes.push(class);
break;
}
}
if(j == students.length) {
students.push({
name: name,
classes: [class]
});
}
}
var ts = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('output');
var output = [];
for(var i = 0; i < students.length; i++) {
output[i] = [students[i].name];
for(var j = 0; j < students[i].classes.length; j++) {
output[i].push(students[i].classes[j].score,
students[i].classes[j].class,
students[i].classes[j].teacher);
}
ts.getRange(i+1,1, 1,output[i].length).setValues([output[i]]);
}
}

I tried this for a while, and came up with a different approach. Below in case someone finds it useful, I also added some comments
function appendToRecords2() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetWithNewData = ss.getSheetByName("New");
var sheetWithExistingData = ss.getSheetByName("Existing")
// Defines the range where the NEW DATA is located
var newData = sheetWithNewData.getRange("a2:b25")
.getValues();
var lastRow = sheetWithExistingData.getLastRow();
sheetWithExistingData.getRange(lastRow + 1, 1, 24, 2)
.setValues(newData);
/*
Parameters for the last to one row
1st = +1, this is always the same. This indicates that it's last row + 1
2nd = The column where to copy the data, normally 1
3rd = The number of rows that contains the new data. This is pretty much the same as the rage defined in getRange
4th = The number of columns that contains the data, same as above
*/
}
IMPORTANT: All credit goes to #jvdh from his answer at Script to append range of data in Google Sheets. I only made some minor changes and added the comments clarifications

Related

Jumbling data using Javascript

I've dataset which look like this(Raw Data) :-
And I want to jumble or sort the data like Book 1 - Topic A and next row Book 2 - Topic B, so one book-chapter and next book and chapter, if we does that, data should look like this :-
I somehow made it, but it's not getting data which has more chapter, like Book 3 has 3 Chapter and Book 4 has 4 chapter, so it's not extracting that.
Using this code now:-
function jumbling() {
const ss = SpreadsheetApp.getActiveSpreadsheet()
const ssname = ss.getSheetByName('Raw')
const data = ssname.getRange(2,1,ssname.getLastRow()-1,8).getValues()
const lessons = [...new Set(data.map(function(r){return r.slice(3,4)}).flat())]
const arr = []
let j = 0
console.log(lessons)
let thisLesson
let to_Len = 0
for(let i = 0 ; i < lessons.length ; i++)
{
let k = 0
while(k < lessons.length)
{
let thisLesson = data.filter(r=> r[3] === lessons[k])
arr.push(thisLesson[j])
k += 1
}
j += 1
}
console.log(arr)
ss.getSheetByName('Processed').getRange(1,1,arr.length,arr[0].length).setValues(arr)
}
You can try with this formula, if it fits with your intention:
=SORT(A2:H,BYROW(A2:A,LAMBDA(each,IF(each="","",COUNTIF(A2:each,each)))),1)
With BYROW it makes a cumulative count of each time an ID appears (I used A column to configure it, because I guessed that's the value of uniqueness, but can change it to column D if needed).

How to retrieve data from columns dependent upon date

I have a rota (a fixed order of rotation (as of persons or duties)) that I've already had help with this week. It's up & running as is, but for simpler reading I'd like to transpose it.
You can see the transposed sheet as I'd like it here
The current script is for the pre-transposed table.
It would search Column 0 for the date. If it was was 7 days away it would retrieve the name from Column 1 & match it with e-mail address in separate sheet etc.
What I'd like to do is instead have the Date in Row 0 & then subsequent names in Row 1 etc etc
I've tried various things. I've stepped through the code & can see what it's doing, & I've done some reading through about 2 dimensional arrays, but I can't seem to find a way of getting the code to work down through columns, instead of across the rows.
Here's the code:
function sendEmails() {
var ss1 = SpreadsheetApp.getActiveSpreadsheet();
var sh1 = ss1.getSheetByName("Rota")
ss1.setActiveSheet(sh1);
var rotalink = "https://docs.google.com/spreadsheets/d/1LgzUWSAGA2kbpar8r5nosU1bSHF7nrtvtUiHS3nB_e8";
var sheet = SpreadsheetApp.getActiveSheet();
// Fetch the range
var dataRange = sheet.getRange("B3:G50")
// Fetch values for each row in the Range.
var data = dataRange.getValues();
for (i in data) {
var row = data[i];
var today=new Date();
var timecell = new Date(row[0]);
var timediff = new Date();
var one_day=1000*60*60*24;
var daystogo = Math.ceil((timecell.getTime()-today.getTime())/(one_day));
if (daystogo==7) {//only e-mail people with one week to go. To change that alter the "7" to the number of days you want
var subject = "Rota reminder!";
var emailAddress = [];
var message;
message = "Hello \n\n"+
"You are down to help at Youth Café this week. \n\n" +
"Please see the below rota for your role \n\n" +
"If you have any questions or problems let us know at thameyouthcafe#gmail.com \n\n" +
"Remember, you can check the rota anytime by clicking on the link below: \n\n"+
rotalink
for (var x = 1; x < 5; x++) { // 5 because emails are till col4
// var emailAddress = []; // Start by collecting the non-blank emails in an array
if (getEmailFromName(row[x]) != "") {
emailAddress.push(getEmailFromName(row[x]))
}
}
emailAddress = emailAddress.join(); // Join the array to get a comma separated string
MailApp.sendEmail(emailAddress, subject, message);
}
}
}
and here's the getEmailFromName function that matches with SKey (which I presume comes from the "i" variable in the first function?
function getEmailFromName(sKey) {
// to use this function, don’t put anything in the first column (A) or row (1).
// Put the name (i.e. the key, or what we’re looking for) in column B.
// Put what we want to return in column C.
var columnToSearch = 1; //column B
// Set the active sheet to our email lookup
var ss1 = SpreadsheetApp.getActiveSpreadsheet();
var sh1 = ss1.getSheetByName("EmailContactList")
ss1.setActiveSheet(sh1);
var data = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
var line = -1;
for( var i = 0; i < data.length; i++ ) {
if( data[i][columnToSearch] == sKey ) {
line = i;
break;
}
}
if( line != -1 ) {
//do what you want with the data on "line"
return data[line][2]; //value on column C of the matched line
}
else {
return "";
// if criteria is not found
}
}
Try it this way:
function sendEmails() {
var ss1 = SpreadsheetApp.getActive();
var sh1 = ss1.getSheetByName("Rota")
ss1.setActiveSheet(sh1);
var rotalink = "https://docs.google.com/spreadsheets/d/1LgzUWSAGA2kbpar8r5nosU1bSHF7nrtvtUiHS3nB_e8";
var sheet = SpreadsheetApp.getActiveSheet();
var dataRange = sheet.getRange("B3:G50")
var data = dataRange.getValues();
for (var i=0;i<dataRange.length;i++) {
var row = data[i];
var today=new Date();
var timecell = new Date(row[0]);
var timediff = new Date();
var one_day=1000*60*60*24;
var daystogo = Math.ceil((timecell.getTime()-today.getTime())/(one_day));
if (daystogo==7) {
var subject = "Rota reminder!";
var emailAddress = [];
var message = Utilities.formatString('Hello\n\nYou are down to help at Youth Café this week.\n\n Please see the below rota for your role \n\nIf you have any questions or problems let us know at thameyouthcafe#gmail.com \n\nRemember, you can check the rota anytime by clicking on the link below: \n\n%s',rotalink);
for (var x=1;x<5;x++) {
if(data[i][x]) {
emailAddress.push(data[i][x]);
}
}
MailApp.sendEmail(emailAddress.join(), subject, message);
}
}
}
Managed to solve it - thank you for your contributions. Turned out it was incredibly simple.
Just had to change this line:
var timecell = new Date(data[0])
to this:
var timecell = new Date(data[0][i])
so it iterates through the first row of each column.

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]);
}
}
}
}
}
}
}

How do you use a script to delete a row off of a spreadsheet that is linked to a form?

I have this script that does several things with data once it is entered into a google form, but I need to make sue that when two entrants have the exact same name that it deletes the previous entry entirely.
function formChanger() {
var doc = DocumentApp.openById('THIS WAS MY ID');
var body = doc.getBody();
var date = body.getListItems();
var dates = [];
for(var i = 0; i<date.length;i++)
{
dates.push(date[i].getText());
}
var form = FormApp.openById('THIS WAS MY ID');
var items = form.getItems();
var ss = SpreadsheetApp.openById("THIS WAS MY ID");
Logger.log(ss.getName());
var sheet = ss.getSheets()[0];
var values = sheet.getSheetValues(2, 4, sheet.getLastRow() , 1);
Logger.log(values);
var names = sheet.getSheetValues(2, 2, sheet.getLastRow(), 1);
var item = items[2].asMultipleChoiceItem();
var choices = item.getChoices()
for(var i=names.length; i>-1; i--){
for(var j=names.length; j>-1; j--){
if(names[i]==names[j] && i != j)
sheet.deleteRow(i);
}
}
var h = -1;
var j = -1;
var k = -1;
var l = -1;
for(var o = 0; o<values.length; o++){
if(choices[0].getValue().equals(values[o].toString()))
h++;
if(choices[1].getValue().equals(values[o].toString()))
j++;
if(choices[2].getValue().equals(values[o].toString()))
k++;
if(choices[3].getValue().equals(values[o].toString()))
l++;
}
if(h>3)
dates.splice(0,1);
if(j>3)
dates.splice(1, 1);
if(k>3)
dates.splice(2, 1);
if(l>3)
dates.splice(3, 1);
emptyDocument();
Logger.log(h);
Logger.log(j);
Logger.log(k);
Logger.log(l);
item.setChoices([
item.createChoice(dates[0]),
item.createChoice(dates[1]),
item.createChoice(dates[2]),
item.createChoice(dates[3])
]);
for(var i = 0; i<dates.length; i++)
body.appendListItem(dates[i]);
Logger.log(doc.getName()+" Contains:");
Logger.log(dates);
}
Yes the code is a mess, and I'm sure that it could be done a better way, but the important part is that I could be able to delete the line of information that is repeated. The compiler will not allow me to do this because the Spread Sheet is linked to the form. is there a way around this?
The following attempts at deletion are blocked in sheets receiving form data:
deletion of columns with form data
deletion of the row with form questions - that is, row 1
Other rows can be deleted at will. This behavior is exactly the same for scripts as it is for user actions.
Your script attempts to delete row 1 because it's buggy. I quote the relevant part:
var names = sheet.getSheetValues(2, 2, sheet.getLastRow(), 1);
for(var i=names.length; i>-1; i++){
for(var j=names.length; j>-1; j++){
if(names[i]==names[j] && i != j)
sheet.deleteRow(i);
What row is names[i] in? It's in row i+2, because i=0 corresponds to row 2. Yet, you attempt to delete row numbered i, two rows above the intended one.
Besides, i>-1; i++ is absurd; you want i-- there.
Here is a simple script that deletes row with duplicates; it's tested with my form responses. It traverses the contents of "Form Responses 1" sheet from bottom to top; if two rows have the same value in column C, the older one gets deleted. I do take care not to attempt deletion of row 1.
(The reason to do this in bottom-up order is to avoid dealing with rows that moved up because others were deleted.)
function deleteDupes() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Form Responses 1');
var values = sheet.getDataRange().getValues();
for (var i = values.length - 1; i > 1; i--) {
if (values[i][2] == values[i-1][2]) {
sheet.deleteRow(i);
}
}
}

Javascript - Associative Arrays not working?

So I have csv files that basically include lists of students, what school they are in, and what subjects they are taking(ex. chem, spanish, biology, etc). I want to have my program let the user type in a subject area(s) and have the page return the amount of times each subject is being taken.
I then have some javascript that basically takes in some user input from a text field, parse that, and puts it into an array. Then, it imports the csv file and compares the subject columns to what the user inputs, and calculates the amount of time each subject is taking.
My javascript looks like this:
var globalArray = [];
var schoolList = [];
var splitTextInput = [];
var count = 0;
var splitSubjectArea = [];
function myFunction()
{
var textInput = document.getElementById('numb').value;
var needsTrimTextInput = textInput.split(","); //creating an array to store user input
for( var q = 0; q < needsTrimTextInput.length; q++) //getting rid of whitespace in user input
{
splitTextInput[q] = needsTrimTextInput[q].trim();
}
for(var j = 0; j< splitTextInput.length; j++)
{
var sSubjectArea = {};
sSubjectArea[ splitTextInput[j] ] = 0; //assigning the value to 0 to store the count of each subject
}
var fileName = document.getElementById("UniversitySelect").value;
if( fileName.indexOf(".csv") > 0 )
{
d3.csv( "./" + document.getElementById("UniversitySelect").value, bob, counting);
}
function bob(d){
return { Area: d.Area };
}
function counting(error, rows)
{
globalArray = rows;
for( var i = 0; i < rows.length; i++ ) //for the row in the CSV file
{
for( var k = 0; k < splitTextInput.length; k++ ) // loop to go through the different inputed subject areas
{
if( rows[i].Area.toLowerCase().indexOf( splitTextInput[k].toLowerCase() ) > -1)
{
count++; //stores the overall count
sSubjectArea[splitTextInput[k]] += 1;
//console.log(sResearchArea[splitTextInput[k]]);
}
}
}
console.log(rows);
for(r = 0; r < splitTextInput.length; r++)
{
console.log( sSubjectArea[ splitTextInput[r] ] );
}//for
}//function
}//function
I know it partially works, because if I enter in 2 subjects, say Chemistry and Biology, which are taken 3 times each, then the count will be 6. However, I cannot get sSubjectArea to hold the count for each individual subject. What am I doing wrong? When I added the console.log(sSubjectArea[splitTextInput[k]]) line, the output I get is:
1
NaN
2
3
4
NaN
5
6
Array [ Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, 86 more… ]
NaN
6
And I don't really understand why I get those numbers & especially NaN.... can anyone help? I'm really new to javascript, so I might have made some sort of fundamental error in understanding objects, but I can't figure it out. Any help would be much appreciated, thanks!!

Categories

Resources