Automate Costs in Google Sheets from Google Form Checkbox Input - google-apps-script

I'm trying to use Google Forms to collect some information on my clients. The application here is a clinical setting, so the form asks what treatments they would like in the form of a checkbox option. I would then like to automate the sum total of the treatment costs using the forms output.
The issue I'm having is that Google Forms outputs a list of strings in a single cell for this response. I'll add more detail below, but I don't know how to split the string into individual values, lookup that value in a separate column, get the cost, and display only the sum in a separate cell.
I've made a minimal working example in the form of a GSheet, you can find it here.
In that master sheet, you'll find three other sheets; Costs, Form Responses, and Overview.
The Costs sheet is static and only contains a list of items and their costs. This sheet will change on occasion (price updates, removal/addition of items)
The Form Responses sheet will contain the raw output from a Google Form. The column of note here is the Choose Things from the List column, which contains a list of responses.
The Overview sheet will house some redundant info, but it's meant to be a cleaned-up sheet with information. You'll notice a Cost of Things ($) column. In this column, I would like the total sum of all items the response list from the Form Responses sheet.
I can do this in Python easy. I would do it something like this:
costs = {'a': 1, 'b': 2, 'c': 3}
input_items = ['a', 'b', 'c']
x = []
for item in input_items:
x.append(costs[item])
total_sum = sum(x)
How would I do this with Google Sheets? I want to
split a list embedded in a cell
check each list item for its cost in a separate sheet
sum the costs of each item
Please let me know if I need to clarify, I'm not quite sure how to pose the problem using Google Sheets language.
==============
EDIT: Sorry, I just updated the GSheet permissions. It should be viewable to everyone now.

In your situation, how about the following sample script?
Sample script:
function myFunction() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet1 = ss.getSheetByName("Costs");
const sheet2 = ss.getSheetByName("Forms Responses");
const sheet3 = ss.getSheetByName("Overview");
// 1. Retrieve values from "Costs" sheet and create an object.
const values1 = sheet1.getRange("A2:B" + sheet1.getLastRow()).getValues();
const obj1 = values1.reduce((o, [a, b]) => (o[a.trim()] = b, o), {});
// 2. Retrieve values from "Forms Responses" sheet and create an object.
const values2 = sheet2.getRange("A2:C" + sheet2.getLastRow()).getValues();
const obj2 = values2.reduce((o, [a, b, c]) => (o[a.trim() + b.trim()] = c.split(",").reduce((n, e) => (n += obj1[e.trim()] || 0, n), 0), o), {});
// 3. Retrieve values from "Overview" sheet and create an array.
const values3 = sheet3.getRange("A2:B" + sheet3.getLastRow()).getValues();
const res = values3.map(([a, b]) => [obj2[a.trim() + b.trim()] || null]);
// 4. Put array to "Overview" sheet.
sheet3.getRange(2, 3, res.length, 1).setValues(res);
}
In this sample script, the following flow is run.
Retrieve values from "Costs" sheet and create an object.
Retrieve values from "Forms Responses" sheet and create an object.
Retrieve values from "Overview" sheet and create an array.
Put array to "Overview" sheet.
In this case, the result values are put to the column "C".
Note:
This sample script is for your sample Spreadsheet. So, when you change the Spreadsheet, the script might not be able to be used. Please be careful about this.
References:
getValues()
setValues(values)
reduce()
map()

Related

How can I set new values in a range list, from the sums in another range list?

Please excuse my limited knowledge in advance.
In Google Sheets, I have a budget column (K) that sums multiple inputs and displays the most recent totals.
I am trying to create a script that will copy select values from column K and set those values into column I upon execution.
Both column K and I have sum totals in various rows throughout so a simple copy and pasting of the entire column would overwrite those functions. This will be a repeatable action to set new values once they are calculated into this column so I plan to assign it to a button.
I'm using User Tanaike's wonderful RangeListApp to help reference the multiple ranges I'm referencing.
The first half of the code seems to be working:
function UpdateEFC() {
var rangeList = ['K4:K6', 'K9:K14', 'K17:K18', 'K21:K24', 'K27', 'K30:K38', 'K41:K49', 'K52:K53', 'K56:K60', 'K63:K67', 'K70:K72', 'K75:K76', 'K79:K85', 'K88:K92', 'K95:K106', 'K109:K114', 'K117:K126', 'K129:K133', 'K136:K147', 'K150:K153', 'K156:K163', 'K166:K167', 'K170:K174', 'K177', 'K180:K182', 'K185', 'K188:K190', 'K193', 'K196:K205', 'K208:K214', 'K217'];
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var r = RangeListApp.getSpreadsheet(spreadsheet).getRangeList(rangeList).getDisplayValues();
Again, each range listed (i.e. "k4:k6") are sum functions in each cell. When I log this script it returns the calculated value in each cell. Here is an example from the log:
[{range='CR Template'!K4:K6, values=[[ $ 28,000.00 ], [ $ 25,000.00 ], [ $ 27,500.00 ]]},
When I attempt to set these values into column I, however, I run into issues.
I have tried the following:
var destinationList = ["I4:I6", "I9:I14", "I17:I18", "I21:I24", "I27", "I30:I38", "I41:I49", "I52:I53", "I56:I60", "I63:I67", "I70:I72", "I75:I76", "I79:I85", "I88:I92", "I95:I106", "I109:I114", "I117:I126", "I129:I133", "I136:I147", "I150:I153", "I156:I163", "I166:I167", "I170:I174", "I177", "I180:I182", "I185", "I188:I190", "I193", "I196:I205", "I208:I214", "I217"];
var values = rangeList;
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
RangeListApp.getSpreadsheet(spreadsheet).getRangeList(destinationList).setValues(values);
This script returns the cell ranges into column as written in the script. i.e. I4 = K4:K6, I5 = K4:K6, I6 = K4:K6 but does not return the displayed numerical summed value.
I have also tried a version where
var values = r;
My thinking was to take the output of displayed values and set those values into the new range list, however this only returned:
[object Object]
into each cell.
I know I have something wrong here in how to define or recall the values so any guidance here would be appreciated.
Many thanks in advance.
I believe your goal is as follows.
You want to copy the values from ['K4:K6', 'K9:K14', 'K17:K18',,,] to ["I4:I6", "I9:I14", "I17:I18",,,].
Issue and workaround:
In the current stage, unfortunately, my RangeListApp cannot copy the values with the ranges. So, in this case, as a current workaround, I would like to propose using a script with Sheets API. The sample script is as follows.
Sample script:
Before you use this script, please enable Sheets API at Advanced Google services.
function myFunction() {
var sheetName = "Sheet1"; // Please set your sheet name.
// These ranges are from your script.
var srcRangeList = ['K4:K6', 'K9:K14', 'K17:K18', 'K21:K24', 'K27', 'K30:K38', 'K41:K49', 'K52:K53', 'K56:K60', 'K63:K67', 'K70:K72', 'K75:K76', 'K79:K85', 'K88:K92', 'K95:K106', 'K109:K114', 'K117:K126', 'K129:K133', 'K136:K147', 'K150:K153', 'K156:K163', 'K166:K167', 'K170:K174', 'K177', 'K180:K182', 'K185', 'K188:K190', 'K193', 'K196:K205', 'K208:K214', 'K217'];
var dstRangeList = ["I4:I6", "I9:I14", "I17:I18", "I21:I24", "I27", "I30:I38", "I41:I49", "I52:I53", "I56:I60", "I63:I67", "I70:I72", "I75:I76", "I79:I85", "I88:I92", "I95:I106", "I109:I114", "I117:I126", "I129:I133", "I136:I147", "I150:I153", "I156:I163", "I166:I167", "I170:I174", "I177", "I180:I182", "I185", "I188:I190", "I193", "I196:I205", "I208:I214", "I217"];
var ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
var values = Sheets.Spreadsheets.Values.batchGet(ssId, { ranges: srcRangeList.map(e => `'${sheetName}'!${e}`), valueRenderOption: "FORMATTED_VALUE" }).valueRanges;
var data = values.map(({ values }, i) => ({ range: `'${sheetName}'!${dstRangeList[i]}`, values }));
Sheets.Spreadsheets.Values.batchUpdate({ data, valueInputOption: "USER_ENTERED" }, ssId);
}
When this script is run, the values of srcRangeList is copied to dstRangeList.
Note:
I would like to consider updating RangeListApp.
References:
Method: spreadsheets.values.batchGet
Method: spreadsheets.values.batchUpdate

GAS method for Efficiently writing data to a custom bound form in Google Sheets

function Ex_WriteVolDataToForm(){
var Ss=SpreadsheetApp.getActiveSpreadsheet();
var ShUserForm=Ss.getSheetByName("User Contact Info Form");//User Interface Form
var Search_str = ShUserForm.getRange("C5").getValue();//Searches for Last Name (Entered in 'C5' on the user form)
var SearchRowDisplayed = ShUserForm.getRange("A13").getValue();//The row # of the "VolSearchResult Tbl" record that is currently displayed on the form
var NumberOfSearchFindsLeft = ShUserForm.getRange("C10").getValue();//=n if there are 'n' records to display from the search function; ="0" if NO search Records
var SearchResultDatasheet=Ss.getSheetByName("VolSearchResult Tbl");//Destination Table of data for Name search
var LstSearchColNumber = SearchResultDatasheet.getLastColumn();
var start = new Date(); //This line before operation begins
ResetDataForm(); //Clears all data from 'User Contact Info Form'
//Re-enter the 'hidden' data cleared by the 'ResetDataForm' fcn
ShUserForm.getRange("C10").setValue(NumberOfSearchFindsLeft);//Re-enters in C10 the number of records found from the "VolSearchResult Tbl" or "" [Null] if NOT from "VolSearchResult Tbl"
ShUserForm.getRange("A13").setValue(SearchRowDisplayed);//Re-enters number of the row displayed from the "VolSearchResult Tbl" (to A12) after being cleared by ResetDataForm function
ShUserForm.getRange("C5").setValue(Search_str);//Re-enters Last Name searched (to C5) after being cleared by ResetDataForm function
var FormRangesToSetValues = ["C11", "F10", "B10", "B14", "B16", "B12", "C17", "F14"]
var RowValues = SearchResultDatasheet.getRange(SearchRowDisplayed,1,1,LstSearchColNumber).getValues();//Creates an Array 1x56 items
for (var i=0; i<LstSearchColNumber; i++) { //adjusted for the starting column is '0', so the last column (index) is LstColNumber-3 NOTE: Excludes cells 'BB3' and 'BC' from routine
ShUserForm.getRange(FormRangesToSetValues[i]).setValue(RowValues[0][i]);// Sequentially sets values in form cells from corresponding values in the 'RowValues' array
if (RowValues[0][i]!="") { //True if there IS a value
ShUserForm.getRange(FormRangesToSetValues[i]).setBackground('#dff3ef'); //Sets Cell Bkg color for all item Not equal to "" to lt. green (ALL form cells with data turn Lt. Green)
}
SpreadsheetApp.flush();
}
ShUserForm.activate();//Takes user to the 'Volunteer Information Form'
return //Exit if only one record found
}
I'm looking a method to accelerate the processing speed. The whole source spreadsheet is 56 columns of data with records of personal data in each row. The search from one sheet and write to the search results sheet runs pretty quickly. The REALLY slow part of the process is writing the data from the search results sheet to the form (you'll note that the order on the destination form is not the same sequence as the sheet data). Generating the array of data (read from the sheet record) proceeds at a decent speed. Writing the array data to the form (all 56 fields + a few constant values I use for other operations) measured at 54 seconds. As I understand it, the 'batch' method is only good for string data (there are few fields of numerical data and dates in the records). If you can get me pointed in the right direction on this problem, I would appreciate it. Thanks for any help in advance.
//This function copies the data of the current Google Sheets record to the User Form
//** Abbreviated version of the function I'm using to copy data from a Google sheet to a custom form (created on another Google sheet) in the same spreadsheet. */
I believe your goal is as follows.
You want to reduce the process cost of your script.
In this case, I would like to propose using Sheets API and the range list for your script. When your script is modified, please modify it as follows.
Modified script:
Before you use this script, please enable Sheets API at Advanced Google services. Ref
From:
for (var i=0; i<LstSearchColNumber; i++) { //adjusted for the starting column is '0', so the last column (index) is LstColNumber-3 NOTE: Excludes cells 'BB3' and 'BC' from routine
ShUserForm.getRange(FormRangesToSetValues[i]).setValue(RowValues[0][i]);// Sequentially sets values in form cells from corresponding values in the 'RowValues' array
if (RowValues[0][i]!="") { //True if there IS a value
ShUserForm.getRange(FormRangesToSetValues[i]).setBackground('#dff3ef'); //Sets Cell Bkg color for all item Not equal to "" to lt. green (ALL form cells with data turn Lt. Green)
}
SpreadsheetApp.flush();
}
To:
var data = [];
var rangeList = [];
for (var i = 0; i < LstSearchColNumber; i++) {
data.push({range: `'User Contact Info Form'!${FormRangesToSetValues[i]}`, values: [[RowValues[0][i]]]});
if (RowValues[0][i] != "") {
rangeList.push(FormRangesToSetValues[i]);
}
}
Sheets.Spreadsheets.Values.batchUpdate({data: data, valueInputOption: "USER_ENTERED"}, Ss.getId());
ShUserForm.getRangeList(rangeList).setBackground('#dff3ef');
References:
Method: spreadsheets.values.batchUpdate
getRangeList(a1Notations)

Delete Row if Match found from another spreadsheet - Google Sheets

I would like to delete any rows of data from spreadsheet 2 if a Match is found in spreadsheet 1.
In the image below (spreadsheet 1) we have SKU A10114 & New Location J05A1.
In the below Image(Spreadsheet 2) here you can see SKU A10114 at Location J05A1 has 2 line entries.
So the code would delete both lines of A10114 at Location J05A1 ONLY
If A10114 had a different location it would not be deleted
I believe your goal as follows.
You want to delete rows in Sales sheet, when the values of columns "A" and "F" in Relocation sheet are included in the values of columns "A" and "B" in Sales sheet.
For this, I would like to propose the following flow.
Retrieve the values from Relocation sheet and create an object for searching values.
Retrieve the values from Sales sheet and create an array for deleting the rows.
Delete rows.
When above flow is reflected to the script, it becomes as follows.
Sample script:
function myFunction() {
const ss = SpreadsheetApp.getActive();
// 1. Retrieve the values from `Relocation` sheet and create an object for searching values.
const sheet1 = ss.getSheetByName('Relocation');
const valuesSheet1 = sheet1.getRange("A1:J" + sheet1.getLastRow()).getValues()
.reduce((o, [a,,,,,f]) => Object.assign(o, {[a + f]: true}), {});
// 2. Retrieve the values from `Sales` sheet and create an array for deleting the rows.
const sheet2 = ss.getSheetByName('Sales');
const valuesSheet2 = sheet2.getRange("A2:B" + sheet2.getLastRow()).getValues()
.reduce((ar, [a,b], i) => {
if (valuesSheet1[a + b]) ar.push(i + 2);
return ar;
}, []).reverse();
// 3. Delete rows.
valuesSheet2.forEach(r => sheet2.deleteRow(r));
}
Note:
When I saw your script in your shared Spreadsheet, from the sheet names in the script, I thought that your script might not be related to this question. So I proposed above sample script.
References:
reduce()
deleteRow(rowPosition)

How do you apply a JOIN and FILTER formula to an entire column in Google Sheets using Google Script?

I'm very new to Google Scripts so any assistance is greatly appreciated.
I'm stuck on how to apply my formula which uses both JOIN and FILTER to an entire column in Google Sheets.
My formula is: =JOIN(", ",FILTER(N:N,B:B=R2))
I need this formula to be added to each cell in Column S (except for the header cell) but with 'R2' changing per row, so in row 3 it's 'R3', row 4 it's 'R4' etc.
This formula works in Google sheets itself but as I have sheet that is auto replaced by a new updated version daily I need to set a google script to run at certain time which I can set up via triggers to add this formula to my designated column.
I've tried a few scripts I've found online but none have been successful.
If you want to solve this using only formulas:
Since your formula is always in the format:
=JOIN(", ",FILTER(N:N,B:B=R<ROW NUMBER>))
and you want to apply it to a very large number of rows, you can use INDIRECT and ROW to achieve a dynamic formula. This answer has a good example on how to use this.
Using formulas you don't risk running into time limits with Apps Script
In practical terms, if you have your data on column A, you can write =ARRAYFORMULA(CONCAT("R",ROW(A2:A))) to get something like this:
Your final formula should look like this:
=JOIN(", ",FILTER($N:$N,B:B=INDIRECT(CONCAT("R",ROW($R2)))))
Also, you can drag it down to other cells like any other formula!
Set the formulas through Apps Script:
You can use setFormulas(formulas) to set a group of formulas to all the cells in a range. formulas, in this case, refers to a 2-dimensional array, the outer array representing the different rows, and each inner array representing the different columns in each specific row. You should build this 2D array with the different formulas, while taking into account that the row index from R should be different for each single formula.
You could do something like this:
function settingFormulas() {
var sheet = SpreadsheetApp.getActive().getSheetByName("Sheet1");
var firstRow = 2;
var column = 19; // Column S index
var range = sheet.getRange(firstRow, column, sheet.getLastRow() - firstRow + 1);
var formulas = range.getValues().map((row, index) => {
let rowIndex = index + firstRow;
return ["=JOIN(\", \",FILTER(N:N,B:B=R" + rowIndex + "))"];
});
range.setFormulas(formulas);
}
In this function, the optional index parameter from the method map is used to keep track of the row index, and adding it to the formula.
In this function, the sheet name is used to identify which sheet the function has to set the formulas to (in this case, the name's Sheet1). Here I'm assuming that once the sheet is replaced by a newer one, the sheet name remains the same.
Execute this daily:
Once you have this function, you just have to install the time-driven trigger to execute this function daily, either manually, following these steps, or programmatically, by running this function once:
function creatingTrigger() {
ScriptApp.newTrigger("settingFormulas")
.timeBased()
.everyDays(1)
.create();
}
Reference:
setFormulas(formulas)
getRange(row, column, numRows)
Installable Triggers
Instead of the workaround hacks I implemented a simple joinMatching(matches, values, texts, [sep]) function in Google Apps Script.
In your case it would be just =joinMatching(R1:R, B1:B, N1:N, ", ").
Source:
// Google Apps Script to join texts in a range where values in second range equal to the provided match value
// Solves the need for `arrayformula(join(',', filter()))`, which does not work in Google Sheets
// Instead you can pass a range of match values and get a range of joined texts back
const identity = data => data
const onRange = (data, fn, args, combine = identity) =>
Array.isArray(data)
? combine(data.map(value => onRange(value, fn, args)))
: fn(data, ...(args || []))
const _joinMatching = (match, values, texts, sep = '\n') => {
const columns = texts[0]?.length
if (!columns) return ''
const row = i => Math.floor(i / columns)
const col = i => i % columns
const value = i => values[row(i)][col(i)]
return (
// JSON.stringify(match) +
texts
.flat()
// .map((t, i) => `[${row(i)}:${col(i)}] ${t} (${JSON.stringify(value(i))})`)
.filter((_, i) => value(i) === match)
.join(sep)
)
}
const joinMatching = (matches, values, texts, sep) =>
onRange(matches, _joinMatching, [values, texts, sep])

Find and replace the text in a cell with Google Script and Google Sheets

I have a Google Sheet where I collect responses and another tab where I see a report for each record.
I want to add manually a number in the cell Writing Points (as in the image below) and click Update to update the cell of that specific record in the responses tab.
I managed to get the row number depending on the student the formula is:
MATCH($D$3,Responses!D:D, 0)
And the column is always BG of the repsonses tab.
How can I achieve this through Google script? I have an idea but I don't know how to do it.
My try:
row = MATCH($D$3,Responses!D:D, 0)
SpreadsheetApp.getActiveSheet().getRange('BG'+row).setValue(newwritingpoints);
I don't know how to conver the match formula into Google script syntax and how to copy and paste the value from that cell to the responses tab.
Google Sheet link:
https://docs.google.com/spreadsheets/d/1dE6UVABhVp7WqEFdC0ptBK2ne6wEA6hCf0Wi6PxcMWE/edit#gid=2032966397
I believe your goal as follows.
When Update() of Google Apps Script is run by the button, you want to retrieve the cells "D3" and "H24" in the sheet Report.
You want to search the value of "D3" from the column "D" in the sheet Responses.
When the value of "D3" and the values of column "D" in the sheet Responses are the same, you want to put the value of "H24" to the same row of the column "BG" in the sheet Responses.
For this, how about this answer? The flow of this sample script is as follows.
Retrieve the values from the cells "D3" and "H24" in the sheet Report.
Retrieve the values from the cells "D2:D" in the sheet Responses.
Create the range list from the retrieved values.
Put the value using the range list.
Sample script:
function Update() {
var spreadsheet = SpreadsheetApp.getActive();
// 1. Retrieve the values from the cells "D3" and "H24" in the sheet `Report`.
var reportSheet = spreadsheet.getSheetByName("Report");
var searchText = reportSheet.getRange("D3").getValue();
var writingPoints = reportSheet.getRange("H24").getValue();
// 2. Retrieve the values from the cells "D2:D" in the sheet `Responses`.
var responsesSheet = spreadsheet.getSheetByName("Responses");
var values = responsesSheet.getRange("D2:D" + responsesSheet.getLastRow()).getValues();
// 3. Create the range list from the retrieved values.
var ranges = values.reduce((ar, [d], i) => {
if (d == searchText) ar.push("BG" + (i + 2));
return ar;
}, []);
// 4. Put the value using the range list.
responsesSheet.getRangeList(ranges).setValue(writingPoints);
}
In this script, it also supposes the case that the value of "D3" might find at several rows in the sheet Responses.
References:
reduce()
Class RangeList