Google Apps Script, Google Sheets, SpreadsheetApp, 2d arrays
<< back to Introduction Part 2 >>
Hey, you made it! It’s time to do some coding. You know, the fun stuff.
In this part of the 2-dimensional data transformation course, we are going to get the total count of results of some form data on a Google sheet. The form data contains a single question item with a selection of different unordered choices.
To get the most accurate data we could we surveyed goats online and asked them what type of goat they are. Here is a sample of the results:
You can grade the data for Part 1 by following this link:
Part 1: 2d array count data Google Sheet
Table of Contents
Goal: Count the frequency of each choice
Our goal is to use Google Apps Script to grab the question and all the choices in column B and then display the total count of each choice. Column A is just the form submission id, so we can ignore that.
Of course, we could do this pretty easily with Google Sheets alone using a COUNTIF function, but that’s not the purpose of this course. This first step is a base for our workflow to expand into more complicated versions of the same count data transformation.
We’ll return the count data back to the Google Sheet in two different ways so that we can get a better understanding of transforming 2d array data.
- Item question header, subheader choices:
- Choices header, column A – questions:
Being a list of goat breeds, we don’t really need to order the list. This will simplify things in our first step.
We are going to display this count in the same sheet that our source data is in, but we will set up our code so that we can change that easily; just in case we want to expand to another sheet later.
The Code
The code is split into two main functions. The runsies()
function and the count transformation function. The count transformation function will either be the singleGroup_SingleQuestionHorizontal(question,results)
, or the singleGroup_SingleQuestionVertical(question,results)
function depending on which one you choose.
runsies()
This is the main on run function for our code. This function will contain all the variables you will need to bring in from the Google Sheet to transform your data. We will need to collect the range of data and the question item here.
We will also need to call the Google Apps Script SpreadsheetApp class so that we can grab the data and set it back into the sheet once it is transformed.
The runsies()
function will also call one of our two count data transformation function.
Let’s take a look at runsies()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/*##### Display the count of a single question's selections ##### * This is the main run function. * * This code takes choice data as a 2d array from a Google Sheet and get's the total count of * each choice before returning a new 2d array in two formats. */ function runsies(){ //### INPUT ############### //Add your choices here var QUESTION_RANGE = "B2"; var RESULT_RANGE = "B3:B26"; var DISPLAY_LOCATION_START = "E3"; //Where you want to paste your transformed data. var SOURCE_SHEET_NAME = "Count from Choice Step 1"; var DESTINATION_SHEET_NAME = "Count from Choice Step 1"; //######################### var ss = SpreadsheetApp.getActiveSpreadsheet(); var sourceSheet = ss.getSheetByName(SOURCE_SHEET_NAME); //Collect data values var resultsVals = sourceSheet.getRange(RESULT_RANGE).getValues(); var questionVals = sourceSheet.getRange(QUESTION_RANGE).getValue(); //Run the data transformation. var displayResults = singleGroup_SingleQuestionHorizontal(questionVals,resultsVals); //var displayResults = singleGroup_SingleQuestionVertical(questionVals,resultsVals); //Paste the data. var destinationSheet = ss.getSheetByName(DESTINATION_SHEET_NAME); pasteResults(DISPLAY_LOCATION_START,displayResults,destinationSheet); }; |
Input variables
From lines 9 to 18 we’ve marked out all the input variables we want to use for our project. Down the track, you might like to create a sidebar or input box so that the user can input this data from the Google Sheet. However, for now, we will add in our cell, range and Google Sheet locations here directly into our Google Apps Script.
Having all these input variables in one location makes it easier to make any changes later on. This also makes it easier for other users to see what your main variables are at a glance and modify them to their own use.
Take a look at the Google Sheet image below to see what we are referencing:
As you can see, we select the QUESTION_RANGE
item in B2. We also grab all the responses in our RESULTS_RANGE
in B3:B26. Then, we set where we want to paste our transformed count data starting from cell E3. The results will be pasted in the same Google Sheet tab this time so the SOURCE_SHEET_NAME
and DESTINATION_SHEET_NAME
will reference the same sheet tab.
Spreadsheet App Class
Next, we call the SpreadsheetApp class and ask Google Apps Script to look at our active spreadsheet – the one we currently are working on – with the getActiveSpreadsheet() method (Line 20). We will pack this into our ss
variable.
Using our ss variable we will ask Google Apps Script to focus on the current sheet with the getSheetByName(SOURCE_SHEET_NAME)
. You can see here that we are calling in the file name we entered in our input variables earlier. SOURCE_SHEET_NAME
will be “Count from Choice Step 1”. (Line 21)
Grabbing the values from the Google Sheet
Next, we are going to bring in the values from our range of results and our question item cell and save them in a variable each. First, we need to get each of the ranges using the locations we provided earlier in our input using the getRange() method. Once you collect the range of an item you can do all sorts of things with that range. You can grab its values, edit its formatting, change its properties and much more. For us, all we want to do is get the values for each of our ranges.
We use the getValues() method for this. The range values will look a little like this:
1 2 3 4 5 6 7 8 9 10 |
resultsVals = [ ["Pygme"], ["Anglo-Nubian"], ["Boer"], ["Anglo-Nubian"], ["Saanen"], [...etc] ]; questionVals = "What type of goat are you?" |
Note, that when more than one cell is referenced, getRange
produces a 2-dimensional array (see resultsVals
above). Each item of the outer array is the row. Inside the inner array contains the column values. Alternatively, when a single cell is referenced, like with questionVals
, then a string is stored. (Lines 23-25)
transforming the data to a 2-d count array
Now that we have our data range values, we need to get the count of each choice in our survey. We have two variables here. We commented out the second one so that it won’t be called. Each of the variables calls our functions that will transform our data into the total count of each item selected in the survey form:
23 24 25 |
//Run the data transformation. var displayResults = singleGroup_SingleQuestionHorizontal(questionVals,resultsVals); var displayResults = singleGroup_SingleQuestionVertical(questionVals,resultsVals); |
Both the variables take two arguments, our question and our results data. Once the function is run it will return the count of each response item in our results as the variable displayResults
. (Lines 28-30)
Pasting the count data to the google sheet
Lastly, we need to display our count data back into our Google Sheet. We’ll grab our destination start location range first with the getRange()
method.
The final step is sending our transformed count data to our pasteResults() function. The pasteResults()
function is a small helper function we created to get the end cell of the data to be pasted and then paste in the data. It takes 3 arguments: the DISPLAY_LOCATION_START
cell, the newly created displayResults
and the destinationSheet
.
singleGroup_SingleQuestionHorizontal(question, results)
When this function is selected, the singleGroup_SingleQuestionHorizontal(question,results) function gets the count of each item in the results. It then creates a 2d array with the question item as the header and the choice items as the subheader. The count or frequency of each selection is displayed under each subheader item.
Let’s take a look at how our source data array (From) and our final count data array (To) look Google Apps Script-side.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
From: [ [Question], [response], [response], [response] ] To: [ [Question , , , ], [Response type 1, Response type 2, ... , Response type #], [Response count1, Response count2, ... , Response count#] ] */ |
Looking at our final count data 2d array we will need to add some extra spaces to our question row. Otherwise, Google Apps Script will get cranky when we try and paste our data with an unequal range.
This function has two parameters, the question item and the result items. These are derived from the questionVals
and resultsVals
respectively back in the runsies()
function. It will return the count data 2d array.
Take a look at the code:
Note! Feel free to pop out this code in a separate window. It makes it easier to follow along.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
/*##################################################################### * Choice for single group single question count of choices. * * Creates a 1st header containing the question, a subheader with the response types and then a * row of count data. * * @param {string} question : The question item * @param {array} results : 2d array of question results. * * @returns {array} two row 2d array containing question, choices and count for each choice. */ /* function singleGroup_SingleQuestionHorizontal(question,results){ //Main result in the new order. Preset with Question on first row. var displayResults = [[question],[],[]]; // Loop through list of results for(var res = 0; res < results.length; res++){ // If a response item already exists add 1 to corresponding column. var responseTypeMatch = false; for(var resp = 0; resp < displayResults[1].length; resp ++){ if(results[res][0] === displayResults[1][resp]){ responseTypeMatch = true; //Add to the count ++displayResults[2][resp]; }; }; //If a new response type, add the response type to new column and add 1. if(responseTypeMatch === false){ displayResults[0].push(""); displayResults[1].push(results[res][0]); displayResults[2].push(1); }; }; displayResults[0].pop() return(displayResults); }; |
Display results
The end results of our count 2d array transformation will be packaged in the displayResults
variable in this function which will then be returned to the runsies()
main function.
We first start by setting up our three rows inside our main parent array (line 18). The first child array is the header, so we may as well add the question item to this now. Now, our first row contains the question item and our second row is empty ready to take the subheading of each choice item our code finds. Our third row is also empty ready to take the count of each item.
looping through the rows
On each row of our data, we want to check to look at the choice item. If we have already listed it in row two of our displayResults
then we just want to add 1 to the total count on row 3 of the corresponding result item.
Otherwise, if the response item is not in our new displayResults
list in row two, then we want to add it. We then want to create a new column in our main header next to the question and place a 1 in the count on column 3.
Line 21 starts our for loop through our response data variable results
.
We want to be able to tell our code that response type already exists in our displayResults
or not. We first create responseTypeMatch
, setting it to false before we check the second row of our displayResults
(line 24).
Next, we start our loop through our newly created displayResults
. Here, we want to check all the column items on row [1], so we reference the length of the child array – displayResults[1].length
.
Have we already added the response item?
The first thing we want to check is if we have already added the current response item to our displayResults[1]
list (Line 27). If there is a match, then we set the responseTypeMatch
to true (Line 28) and add one (1) to the count on row displayResults[2]
(Line 31).
What do we do if the response item doesn’t exist?
If we have looped through all our displayResults[1]
response items and the item does not exist, we then need to:
- Expand our header column by 1 to take in the new item
- Create a new response item in
dislpayResults[1]
- Add the first count to the count row for that new item
Because our responseTypeMatch
boolean was not changed to true, we know there is not a match, we can use this value as our if statement.
Every new response item will go at the end of our response row (displayResults[1]
), so we can simply use the push method which adds one or more elements to the end of an array. We push an empty string element to the header, the item response to the subheader and a one to the count row(Lines 90-95).
The displayResults[1]
will now be ready for the next loop through the results data to catch any response item that matches it.
A little problem to resolve
Once all the items and their count have been added after we have iterated through the results loop, there will be one slight problem. Our header will be longer by 1 element. To resolve this we use the pop() method to remove the extra element on the end of our header row, displayResults[0]
. Now we have nice matching columns.
Finally, our newly formed 2d count array is returned back to our runsies()
function.
Let’s take a look at our alternate count layout…
singleGroup_SingleQuestionVertical(question, results)
When this function is selected, the 2d count array starts with the header of response items starting on the second column. On the second row, the question item is first displayed followed by item count frequency for each response.
This function has two parameters, the question item and the result items. It will return the count data 2d array.
Take a look at the 2d array we need to construct in Google Apps Script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* From: [ [Question], [response], [response], [response] ] To [ [" ", Response type 1, Response type 2, ... , Response type #], [Question, Response count1, Response count2, ... , Response count#] ] */ |
You can see that we will need to add an empty string as the first element of the header, and then add the question as the first element of the second row array.
Check out the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
/*##################################################################### * Choice for single group single question count of choices. * * Creates a header of row type and the next row of response count with the associated question in * the first colum. * * @param {string} question : The question item * @param {array} results : 2d array of question results. * * @returns {array} two row 2d array containing question, choices and count for each choice. */ function singleGroup_SingleQuestionVertical(question,results){ //Main result in the new order. Preset with space on first row and question on 2nd. var displayResults = [[""],[question]]; // Loop through list of results for(var res = 0; res < results.length; res++){ // If a response item already exists add 1 to corresponding column. var responseTypeMatch = false; for(var resp = 1; resp < displayResults[0].length; resp ++){ if(results[res][0] === displayResults[0][resp]){ responseTypeMatch = true; //Add to the count ++displayResults[1][resp]; }; }; //If a new response type, add the response type to new column and add 1. if(responseTypeMatch === false){ displayResults[0].push(results[res][0]); displayResults[1].push(1); }; }; return(displayResults); }; |
Display results
Again we set our returned results variable to displayResults
. We immediately add an empty string element to the start of our header row and the question to the start of our count row. (Line 16).
looping through the rows
We then loop through our rows of results (Line 19). We will be comparing these again with what we have added to our displayResults[0]
header row. However, before we do that we need a way to tell our code that the response item already exists. We will do this by creating the responseTypeMatch
and presetting it to false.
Have we already added the response item?
This is the same as our previous singleGroup_SingleQuestionHorizontal(question,results)
function. If we find a match, we will add one to the corresponding count row, displayresults[1]
(Line 25026). responseTypeMatch
will also be set to true (Line 29).
What do we do if the response item doesn’t exist?
Again very similar to our previous function. This time around, however, we don’t need to push empty rows to a question header. We simply add the response item to the question row and add 1 to the corresponding count row. (Lines 34 – 37).
Finally, the displayResults
2d array will be returned to the runsies()
function.
pasteResults(StartLoc, results, sheet)
Back in our main runsies()
function at the very end, we used a helper function we named pasteResults()
. This function has 3 parameters:
- The start location cell in A1 notation of where we want to paste the item.
- The new 2d array we created
- The new sheet tab location (Note, this is not the sheet name but the sheet called from Google Apps Script using the getSheetByName() method)
This is a pretty versatile tool if you doing a lot of data transformation and want to quickly paste in a new 2d array without having to worry about any formatting.
Let’s take a look at the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/*##################################################################### * Pasting results * * @param {string} StartLoc : The start cell of the location you want to paste to. * @param {array} results : 2d array of question results. * @param {object} sheet : The Sheet item. * */ function pasteResults(StartLoc, results, sheet){ var endLoc = sheet.getRange(StartLoc) .offset(results.length-1,results[0].length-1) .getA1Notation(); var range = sheet.getRange( StartLoc+":"+endLoc); range.setValues(results); }; |
endloc
Before we can set our new values into a new Google Sheet tab (or the same one in our case) we need to get the A1 notation (e.g. B9) of the last cell. We’ll do this here by using offset, but there are a number of ways of doing this. I just thought this was kinda fun.
Our endloc
varialble will be an A1 notation cell position equal to the last row and column in our 2d array when we set it into our sheet. We first need to call the startRange
of the cell using the getRange()
method inserting our start location as our argument.
Next, we apply an offset method. Our offset method can take a row offset and a column offset. The offset is how many rows and/or column away from the original location we would like to find. Our offset will be the depth of our 2d array rows minus 1 by the width of our columns minus 1. (Lines 12-14)
We can then grab the A1 notation of this offset location and store it in our endLoc variable. (Line 14)
building the new range
We can then create the range of the new location by combining the startLoc
parameter with the endLoc
variable in a string.(Line 16)
Lastly, we’ll send our range to the Google Sheet. This is the point where Google Apps Scripts writes the range to the Google Sheet and you will see it appear.
Conclusion
Keeping it all neat
We only have a few functions at the moment with this simple project, but we know we are going to build on this in our further stages of this course as we expand the complexity of our project. We should probably start putting our functions in different files so everything is easier to reference. Don’t worry we will keep it all basic for this course.
Create two extra Google Script files. In your script editor go to File > New > Script file.
(Note: in the new apps script you just need to select the plus sign across from file and select script from the pop-up menu)
Create two new files. Call one Trans.gs for your count transformation functions and the other ToolBox.gs for any helper scripts you have. Put your functions in the following:
- Code.gs
runsies()
- Trans.gs
singleGroup_SingleQuestionHorizontal(question,results)
singleGroup_SingleQuestionVertical(question,results)
- ToolBox.gs
pasteResults(StartLoc, results, sheet)
Nice and neat.
The complete code
Here’s the complete code all together in their respective files:
code.gs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
/*##### Display the count of a single question's selections ##### * This is the main run function. * * This code takes choice data as a 2d array from a google Sheet and get's the total count of * each choice before returning a new 2d array in two formats. */ function runsies(){ //### INPUT ############### //Add your choices here var QUESTION_RANGE = "B2"; var RESULT_RANGE = "B3:B26"; var DISPLAY_LOCATION_START = "E3"; //Where you want to paste your transformed data. var SOURCE_SHEET_NAME = "Count from Choice Step 1"; var DESTINATION_SHEET_NAME = "Count from Choice Step 1"; //######################### var ss = SpreadsheetApp.getActiveSpreadsheet(); var sourceSheet = ss.getSheetByName(SOURCE_SHEET_NAME); //Collect data values var resultsVals = sourceSheet.getRange(RESULT_RANGE).getValues(); var questionVals = sourceSheet.getRange(QUESTION_RANGE).getValue(); //Run the data transformation. var displayResults = singleGroup_SingleQuestionHorizontal(questionVals,resultsVals); //var displayResults = singleGroup_SingleQuestionVertical(questionVals,resultsVals); //Paste the data. var destinationSheet = ss.getSheetByName(DESTINATION_SHEET_NAME); pasteResults(DISPLAY_LOCATION_START,displayResults,destinationSheet); }; |
Trans.gs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
/*##################################################################### * Choice for single group single question count of choices. * * Creates a 1st header containing the question, a subheader with the response types and then a * row of count data. * * @param {string} question : The question item * @param {array} results : 2d array of question results. * * @returns {array} two row 2d array containing question, choices and count for each choice. */ /* From: [ [Question], [response], [response], [response] ] To: [ [Question , , , ], [Response type 1, Response type 2, ... , Response type #], [Response count1, Response count2, ... , Response count#] ] */ function singleGroup_SingleQuestionHorizontal(question,results){ //Main result in the new order. Preset with Question on first row. var displayResults = [[question],[],[]]; // Loop through list of results for(var res = 0; res < results.length; res++){ // If a response item already exists add 1 to corresponding column. var responseTypeMatch = false; for(var resp = 0; resp < displayResults[1].length; resp ++){ if(results[res][0] === displayResults[1][resp]){ responseTypeMatch = true; //Add to the count ++displayResults[2][resp]; }; }; //If a new response type, add the response type to new column and add 1. if(responseTypeMatch === false){ displayResults[0].push(""); displayResults[1].push(results[res][0]); displayResults[2].push(1); }; }; displayResults[0].pop() return(displayResults); }; /*##################################################################### * Choice for single group single question count of choices. * * Creates a header of row type and the next row of response count with the associated question in * the first colum. * * @param {string} question : The question item * @param {array} results : 2d array of question results. * * @returns {array} two row 2d array containing question, choices and count for each choice. */ /* From: [ [Question], [response], [response], [response] ] To [ [" ", Response type 1, Response type 2, ... , Response type #], [Question, Response count1, Response count2, ... , Response count#] ] */ function singleGroup_SingleQuestionVertical(question,results){ //Main result in the new order. Preset with space on first row and question on 2nd. var displayResults = [[""],[question]]; // Loop through list of results for(var res = 0; res < results.length; res++){ // If a response item already exists add 1 to corresponding column. var responseTypeMatch = false; for(var resp = 1; resp < displayResults[0].length; resp ++){ if(results[res][0] === displayResults[0][resp]){ responseTypeMatch = true; //Add to the count ++displayResults[1][resp]; }; }; //If a new response type, add the response type to new column and add 1. if(responseTypeMatch === false){ displayResults[0].push(results[res][0]); displayResults[1].push(1); }; }; return(displayResults); }; |
ToolBox.gs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/*##################################################################### * Pasting results * * @param {string} StartLoc : The start cell of the location you want to paste to. * @param {array} results : 2d array of question results. * @param {object} sheet : The Sheet item. * */ function pasteResults(StartLoc, results, sheet){ var endLoc = sheet.getRange(StartLoc) .offset(results.length-1,results[0].length-1) .getA1Notation(); var range = sheet.getRange( StartLoc+":"+endLoc); range.setValues(results); }; |
Last Bit
The current code is pretty useful for when we don’t know what results are going to come in. Our code can just add a response item every time a new one is found. But what if we have a form where we are recording results from a scale, like rating data? We would come across some sorting problems using the same code. Our rating range in our header will be all over the place. We don’t want our response items to be unordered.
In the next tutorial, we will tackle rating data and look at what things in our code we need to change to meet this new problem.
<< back to the course Introduction || On to part 2 >>
Happy coding!
Need help with Google Workspace development?
Go something to solve bigger than Chat GPT?
I can help you with all of your Google Workspace development needs, from custom app development to integrations and security. I have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.
Schedule a free consultation today to discuss your needs and get started or learn more about our services here.
~Yagi