Google Apps Script, Google Sheets, SpreadsheetApp, 2d arrays
In our previous tutorial, we created a 2d array of count values for each item chosen in a survey form in Google Apps Script. In our survey, we asked users to submit what type of goat they are. We didn’t know what species of goat they identified as so we just needed to count for any goat species that was submitted.
In part 2 of our course, our Google Sheets survey data is a little different. This time around we are asking the human companion of their coding goat to:
Rate your goat’s athleticism.
Respondents then rate their goat’s athleticism on a 5-point scale:
- Weak
- Below Average
- Average
- Better Than Average
- Strong
But, Yagi! Can’t we simply use the script in part one?
Sure, you could. However, you might come across a bit of a problem. In Part 1 we generated our choices for our count based on their appearance in the survey. What happens if none of the respondents rated their goat as Weak (This is right and just)? Weak would not be recorded in our 2d count array when we ran our Google Apps Script code.
Further, if the first user in our Google Sheet response data rates their goat as Strong, then the first choice in our 2d count array will be Strong.
That would just look weird for a summary count of a rating survey. We really need to display our count in order from 1.Weak through to 5.Strong.
Table of Contents
Goal: Count the Frequency of Each Rating Choice
Our goal for this task is to get the count for each rating option rating our goat’s athleticism.
Here is what the raw response data looks like in our Google Sheet.
You can access the Google Sheet with the data here:
Either go to File > Make a copy of the Google Sheet or copy and paste the data into your own Google Sheet.
We will need to be able to display our count data with the ratings in the correct order from 1. Weak to 5. Strong.
Again, we will display our data in two ways:
- Item question header, subheader choices:
- Choices header, column A – questions:
Reinvent the Wheel?
One of the aims of this course is to show you how you can reuse parts of your old code in new projects to help speed up your coding.
We can safely say that the 2d count array we created in Part 1 is similar to what we want to do in this new project. There is no point looking at the problem from scratch when we have a similar template available to us.
So before we get started with our new project, let’s copy and paste in our Google Apps Script files from Part 1.
Here they are below for your convenience:
Part 1 files
Code.gs – old
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 – old
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 – Old
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); }; |
As we walk through each file, we’ll identify what we will remove and what we need to add to our script to achieve our rating data count.
The Code
Code.gs
Runsies()
We need to be able to ensure that the rating choices are displayed in the correct order.
One approach to do this is to present the choice items in the code like this:
var CHOICES = ["1.Weak","2.Below Average","3.Average","4.Better Than Average","5.Strong"];
We know that these will be static for this question type and we want them in this particular order so creating a CHOICES
variable seems logical.
Down the track, you might want to draw the choice items from a sidebar option or a Notes section in the Google Sheet so that your users can update where necessary.
We’ll also have an extra parameter for choices in our count transformation functions that return our DisplayResults
. Add CHOICES
as the third argument for these two function options. I’ve also slightly changed the function names. Please go ahead and change them too.
These will be the only changes to our runsies()
function. Our code will now look like this:
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 |
/* * Displaying a count of each selection based on a rating system with a set range. * * Contains a */ /*##### Display the counge of a single question's selections ##### * This is the main run function. */ function runsies(){ //### INPUT ############### //### Add your choices here var QUESTION_RANGE = "B2"; var RESULT_RANGE = "B3:B26"; var CHOICES = ["1.Weak","2.Below Average","3.Average","4.Better Than Average","5.Strong"]; var DISPLAY_LOCATION_START = "D9"; var SOURCE_SHEET_NAME = "Rating Step 2"; var DESTINATION_SHEET_NAME = "Rating Step 2"; //######################### var ss = SpreadsheetApp.getActiveSpreadsheet(); var sourceSheet = ss.getSheetByName(SOURCE_SHEET_NAME); var resultsVals = sourceSheet.getRange(RESULT_RANGE).getValues(); var questionVals = sourceSheet.getRange(QUESTION_RANGE).getValue(); var displayResults = singleGroup_SingleQuestionVert_Rating(questionVals,resultsVals, CHOICES); //var displayResults = singleGroup_SingleQuestionHor_Rating(questionVals,resultsVals,CHOICES); var destinationSheet = ss.getSheetByName(DESTINATION_SHEET_NAME); pasteResults(DISPLAY_LOCATION_START,displayResults,destinationSheet); }; |
Trans.gs
singleGroup_SingleQuestionHor_Rating(question,results,choices)
Remember our 2d count array needs to look like this once we are done:
1 2 3 4 5 |
[ [Question , , , ], [Response type 1, Response type 2, ... , Response type #], [Response count1, Response count2, ... , Response count#] ] |
This time around, we are going to create our header row and the response count row all set to zero before we create our displayResults
main 2d array variable.
The Rows
For our header
row, we need to create a row that includes four blank (“”) elements after the question item. Likewise, we need to create 5 new elements in our response count row filled with zeroes. The repetitive task alert should be going off, just like it did for us.
We created a helper function just for this task we named newArrayFill(len, element)
. newArrayFill
takes two arguments:
- Length of new array: Here, you put in how many elements you want in your array as a number.
- Element character to be repeated: This is any element that we want to set to be repeated in each element of the array we create.
At the start of our singleGroup_SingleQuestionHor_Rating()
function, add two new variables to create our header and our original response count rows.
1 2 |
var header = [question].concat(newArrayFill(choices.length-1, "")); var countRow = newArrayFill(choices.length,0); |
Next, we will join the header, choices and the count row into our Display results.
For the header, we are using the concat method on the
array to join our array of empty elements to it.
Go ahead and update the display results too:
1 2 |
//Main result in the new order. Preset with Question on first row. var displayResults = [header,choices,countRow]; |
Removing Response Match
We no longer have to keep track of whether the choice has been added or not to the count array. We have all the choices loaded already. This means we can get rid of all the code related to the responseTypeMatch
variable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Loop through list of results for(var res = 0; res < results.length; res++){ // If a response item already exists add 1 to corresponding column. DELETE>>> var responseTypeMatch = false; for(var resp = 0; resp < displayResults[1].length; resp ++){ if(results[res][0] === displayResults[1][resp]){ DELETE>>> responseTypeMatch = true; //Add to the count ++displayResults[2][resp]; }; }; DELETE>>> //If a new response type, add the response type to new column and add 1. DELETE>>> if(responseTypeMatch === false){ DELETE>>> displayResults[0].push(""); DELETE>>> displayResults[1].push(results[res][0]); DELETE>>> displayResults[2].push(1); DELETE>>> }; }; |
Comparing the first characters
Quite often rating choices and responses will be saved with a number along with their description. Just like we have in our list:
Here you can see we that “weak” is assigned 1 and “strong” is assigned 5. Our choice data also retains the numbers but capitalises the first letter of the choice.
1 |
var CHOICES = ["1.Weak","2.Below Average","3.Average","4.Better Than Average","5.Strong"]; |
As you can see, the number is always the first character. To compare the CHOICES with the results to get the count we are simply going to compare the first character for each item in the choices and the results. The first item is the zeroeth item.
Add a zero character reference to both the results[res] [0]and the displayResults[1][resp] loop items. For example:
displayResults[1][resp] => displayResults[1][resp][0]
results[res][0] => results[res][0][0]
1 2 3 4 |
for(var resp = 0; resp < displayResults[1].length; resp ++){ var choiceItemNumber = displayResults[1][resp][0]; if(results[res][0][0] === choiceItemNumber){ |
To tidy things up, we also created the variable choiceItemNumber
for our displayResults
item.
Update your function now.
No more pop
Lastly, we no longer need to remove an element from the header. Go ahead and remove the pop method at the end:
1 |
DELETE>>> displayResults[0].pop() |
By now, your new singleGroup_SingleQuestionHor_Rating()
function should look like this:
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 |
/*##################################################################### * Choice for single group single question count of choices with a scaled rating. * * 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_SingleQuestionHor_Rating(question,results,choices){ var header = [question].concat(newArrayFill(choices.length-1, "")); var countRow = newArrayFill(choices.length,0); //Main result in the new order. Preset with Question on first row. var displayResults = [header,choices,countRow]; // Loop through list of results for(var res = 0; res < results.length; res++){ // If a response item already exists add 1 to corresponding column. for(var resp = 0; resp < displayResults[1].length; resp ++){ var choiceItemNumber = displayResults[1][resp][0]; if(results[res][0][0] === choiceItemNumber){ //Add to the count ++displayResults[2][resp]; }; }; }; return(displayResults); }; |
singleGroup_SingleQuestionVert_Rating(question,results,choices)
Remember our 2d count array needs to look like this once we are done:
1 2 3 4 |
[ [" ", Response type 1, Response type 2, ... , Response type #], [Question, Response count1, Response count2, ... , Response count#] ] |
Let’s go through the editing process again with our singleGroup_SingleQuestionVert_Rating(question,results,choices)
function.
The Rows
Again, we are going to add a header and a count response row. This time around our choices will be in our main header. We will need to put a space in before we display our header though.
For our response count row, we will need to first add the question, followed by a bunch of zeros(0) elements equal to the number of choices. We’ll use our trusty newArrayFill()
to do this. Add these two variables just below the function title:
1 2 |
var header = [""].concat(choices); var countRow = [question].concat(newArrayFill(choices.length,0)); |
Again we use the concat method to join the header
‘s empty single element array to the choices array and the countRow
‘s question to the zeroes array.
For our displayResults
array variable, we only have two rows this time around, the header
and the countRow
. Go ahead and update this variable.
1 |
var displayResults = [header,countRow]; |
Removing Response Match
Again, we don’t have to worry about anything to do with checking if a response exists. The choices are pre-loaded. Go ahead and remove anything related to the responseMatch
variable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
DELETE>>> var responseTypeMatch = false; for(var resp = 1; resp < displayResults[0].length; resp ++){ if(results[res][0] === displayResults[0][resp]){ DELETE>>> responseTypeMatch = true; //Add to the count ++displayResults[1][resp]; }; }; DELETE>>> //If a new response type, add the response type to new column and add 1. DELETE>>> if(responseTypeMatch === false){ DELETE>>> displayResults[0].push(results[res][0]); DELETE>>> displayResults[1].push(1); DELETE>>> }; }; |
Comparing the first characters
We’ll also be comparing just the first character again too. So we need to add a [0]
to our results
and our displayResults
when we are comparing the two during each iteration.
Here is how we did:
1 2 3 4 5 |
// If a response item already exists add 1 to corresponding column. for(var resp = 0; resp < displayResults[1].length; resp ++){ var choiceItemNumber = displayResults[1][resp][0]; if(results[res][0][0] === choiceItemNumber){ |
By now, your new singleGroup_SingleQuestionHor_Rating()
function should look like this:
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 |
/*##################################################################### * Choice for single group single question count of choices with a scaled rating. * * 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_SingleQuestionVert_Rating(question,results,choices){ var header = [""].concat(choices); var countRow = [question].concat(newArrayFill(choices.length,0)); //Main result in the new order. Preset with space on first row and question on 2nd. var displayResults = [header,countRow]; // Loop through list of results for(var res = 0; res < results.length; res++){ // If a response item already exists add 1 to corresponding column. for(var resp = 1; resp < displayResults[0].length; resp ++){ var choiceItemNumber = displayResults[0][resp][0]; if(results[res][0][0] === choiceItemNumber){ //Add to the count ++displayResults[1][resp]; }; }; }; return(displayResults); }; |
ToolBox.gs
newArrayFill()
Remember we used the newArrayFill()
function to help create the extra elements for the header and the response count rows. The function creates a new array with a length of n with a desired character or integer. The function has two parameters:
- len: The length of the desired array.
- fillItem: The item to be displayed in each element of the array.
For example, If I wanted to make an array of length 7 filled with chickens. I would call newArrayFill() like this:
var coup = newArrayFill(7,"chicken")
Which would result in:
coup = ["chicken", "chicken", "chicken", "chicken", "chicken", "chicken","chicken"];
Let’s take a look at the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/*##################################################################### * Creates a new array with selecte fill item. * * @param {number} len : Length of array. * @param {number|string} fillItem : Number or string to fill the array item. * @returns {array} The array the required length with required fill. * */ function newArrayFill(len, fillItem){ var arizzle = []; for(var i = 0; i < len; i++){arizzle[i] = fillItem}; return arizzle; }; |
First, on line 11, we create an empty array.
On line 12 we use a for-loop to create a new element in the array len times with the desired fillItem
.
We then return the final arizzle
(That’s what the cool kids call arrays) back to wherever the function was called.
Go ahead and add this helper function to your ToolBox.gs file under the pasteResults()
function.
Conclusion
The Complete Code
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 38 |
/* * Displaying a count of each selection based on a rating system with a set range. * * Contains a */ /*##### Display the counge of a single question's selections ##### * This is the main run function. */ function runsies1(){ //### INPUT ############### //### Add your choices here var QUESTION_RANGE = "B2"; var RESULT_RANGE = "B3:B26"; var CHOICES = ["1.Weak","2.Below Average","3.Average","4.Better Than Average","5.Strong"]; var DISPLAY_LOCATION_START = "D9"; var SOURCE_SHEET_NAME = "Rating Step 2"; var DESTINATION_SHEET_NAME = "Rating Step 2"; //######################### var ss = SpreadsheetApp.getActiveSpreadsheet(); var sourceSheet = ss.getSheetByName(SOURCE_SHEET_NAME); var resultsVals = sourceSheet.getRange(RESULT_RANGE).getValues(); var questionVals = sourceSheet.getRange(QUESTION_RANGE).getValue(); var displayResults = singleGroup_SingleQuestionVert_Rating(questionVals,resultsVals, CHOICES); //var displayResults = singleGroup_SingleQuestionHor_Rating(questionVals,resultsVals,CHOICES); 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 |
/*##################################################################### * Choice for single group single question count of choices with a scaled rating. * * 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_SingleQuestionHor_Rating(question,results,choices){ var header = [question].concat(newArrayFill(choices.length-1, "")); var countRow = newArrayFill(choices.length,0); //Main result in the new order. Preset with Question on first row. var displayResults = [header,choices,countRow]; // Loop through list of results for(var res = 0; res < results.length; res++){ // If a response item already exists add 1 to corresponding column. for(var resp = 0; resp < displayResults[1].length; resp ++){ var choiceItemNumber = displayResults[1][resp][0]; if(results[res][0][0] === choiceItemNumber){ //Add to the count ++displayResults[2][resp]; }; }; }; return(displayResults); }; /*##################################################################### * Choice for single group single question count of choices with a scaled rating. * * 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_SingleQuestionVert_Rating(question,results,choices){ var header = [""].concat(choices); var countRow = [question].concat(newArrayFill(choices.length,0)); //Main result in the new order. Preset with space on first row and question on 2nd. var displayResults = [header,countRow]; // Loop through list of results for(var res = 0; res < results.length; res++){ // If a response item already exists add 1 to corresponding column. for(var resp = 1; resp < displayResults[0].length; resp ++){ var choiceItemNumber = displayResults[0][resp][0]; if(results[res][0][0] === choiceItemNumber){ //Add to the count ++displayResults[1][resp]; }; }; }; return(displayResults); }; |
ToolBox.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 |
/*##################################################################### * 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); }; /*##################################################################### * Creates a new array with selecte fill item. * * @param {number} len : Length of array. * @param {number|string} fillItem : Number or string to fill the array item. * @returns {array} The array the required length with required fill. * */ function newArrayFill(len, fillItem){ var arizzle = []; for(var i = 0; i < len; i++){arizzle[i] = fillItem}; return arizzle; }; |
Last Bit
This part of the course walked through how to look at your old code and quickly adapt it to a new project. We focussed on what changes would need to be made to get count data on a rating system.
We also added a new helper function to our ToolBox.gs file the newArrayFill()
function. You should be able to use this in your other projects.
In Part 3 we will be increasing the complexity of our project by adding multiple question items with the same rating system. We’ll be building on what we have already learnt to create this. At this stage, you will really start to see the two different 2d count arrays diverge and can start to see the benefits of both.
Subscribe now (Up the top right) so get notified when Part 3 is out!
~Yagi
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.
One thought on “Google Apps Script Course – Part 2: 2D Array Data Transformation of Single Question Item Data to Total Count of Rating Choices in Google Sheets”
Comments are closed.