Google spreadsheet formating based on form responses - google-sheets

I'm trying to make a Google form populate a spreadsheet with multiple rows based on 1 form entered data like this:
Form is simple with minimal customer information's https://docs.google.com/forms/d/1LrKlVuI7kxVU0lxz70Uu-2Obj4x3qIwe6nS-ErzbCAg/
Based on selected number of parts needed by customer (at the moment 1, 2 or 3) Form goes to sections where 1, 2 or 3 are entered by parts name and identification number https://docs.google.com/spreadsheets/d/1kAsgrB-2swL_tWYyXF5GLLX2PkVrkHYkcKr9eaQ8gnI/edit?usp=sharing
After entry i need for Forms to enter data in Sheet (or formatted sheet) as follows:
-Each parts entered (1, 2 or 3) should be on individual row with the same client name in common
Sheet editors can then enter data in extra rows such as "Delivery date" and "Item price"
short:
1 form submitted, 3 questions in form, 3 rows
p.s. cell coloring is used only to point out the common data between rows

One option would be to do the following:
#1. Install onFormSubmit trigger:
Install an onFormSubmit trigger attached to your spreadsheet, so that a function runs every time the form attached to the spreadsheet is submitted (this assumes that your Form is attached to your spreadsheet).
The trigger can be installed either manually, following these steps, or programmatically. To install the trigger programmatically, open a script bound to your spreadsheet by clicking Tools > Script editor, and copy an execute this function once:
function createTrigger() {
var ss = SpreadsheetApp.getActive();
ScriptApp.newTrigger("submitData")
.forSpreadsheet(ss)
.onFormSubmit()
.create();
}
This will fire a function called submitData every time the form is submitted. Next, then, would be to write function, which should append the submitted data in the format you desire.
#2. Function to append submitted data to sheet:
In order to append the data submitted through the Form on the sheet called Formated responses, you need to use the corresponding event object, which contains the submitted data. You can use this to check how many parts are submitted and the values for its corresponding fields. Then, the method appendRow could be used to append this data to the sheet.
It could be something like this:
function submitData(e) {
var sheet = SpreadsheetApp.getActive().getSheetByName("Formated responses");
var res = e.namedValues;
var numberOfParts = res["Number of parts"][0];
var mainFields = [res["Timestamp"][0], res["Client name"][0], numberOfParts];
switch (numberOfParts) {
case '1':
var fieldsOne = [res["Part identification number"][0], res["Part name"][0]];
sheet.appendRow(mainFields.concat(fieldsOne));
break;
case '2':
var fieldsTwo = [res["#1 part identification number"][0], res["#1 part name"][0]];
sheet.appendRow(mainFields.concat(fieldsTwo));
fieldsTwo = [res["#2nd part identification number"][0], res["#2nd part name"][0]];
sheet.appendRow(mainFields.concat(fieldsTwo));
break;
case '3':
var fieldsThree = [res["#1st part identification number"][0], res["#1st part name"][0]];
sheet.appendRow(mainFields.concat(fieldsThree));
fieldsThree = [res["#2nd part identification number"][1], res["#2nd part name"][1]];
sheet.appendRow(mainFields.concat(fieldsThree));
fieldsThree = [res["#3rd part identification number"][0], res["#3rd part name"][0]];
sheet.appendRow(mainFields.concat(fieldsThree));
break;
}
}
Note:
The function submitData could be made simpler (the switch could probably be removed, and a for loop used instead), but the names of the Form fields are not consistent with each other, hindering this option. Because of this, the function has a fair amount of repetition. I'd recommend you to fix the field names and rewrite the function a bit.
Reference:
Installable Triggers

Related

Is there a way to use arrayformula with match/index in google script? Or at least get it to autofill the entire column? Scripts are an option

I have the following formula: =ArrayFormula(INDEX(Items!F2:F,MATCH(C2,Items!E2:E,0)))
I would like to extend it such that the entire C column runs the same formula for values. Please help. If a script is necessary to achieve this, I'd like to explore that option too.
Use Apps Script!
Sheet functions (formulae) work great (especially if you are a master like player0), but I find it much easier to work within Apps Script for anything much more complicated than a simple INDEX MATCH. If you are willing to learn some JavaScript, I highly recommend learning some.
Custom Functions
You can write custom sheet functions in Apps Script that you can call with the traditional =FUNCTION() from a cell.
The way it works is that you write a function in Apps Script that returns a two dimensional array corresponding to the area that it needs to fill.
For example, if wanted a function to fill a 2 x 2 block with 1, you would need to make your function return:
[[1,1],[1,1]]
Or you can write it like this:
[
[1, 1],
[1, 1]
]
Implementing Index Match
There are many ways you can implement it, here is an example.
The example spreadsheet has 2 tabs, "Ledger" and "Items".
The goal of the function that follows is to get the costs of the items from the "Items" tab.
function ledgerIndexMatch(){
// Initializing the location of data
let ss = SpreadsheetApp.getActive();
let ledger = ss.getSheetByName("Ledger");
let source = ss.getSheetByName("Items");
let ledgerRange = ledger.getDataRange();
let sourceRange = source.getDataRange();
// Getting the values into a 2D array
let ledgerValues = ledgerRange.getValues();
let sourceValues = sourceRange.getValues();
// Discarding the first row (headers)
ledgerValues.shift();
sourceValues.shift();
// Initializing the output array
let output = [];
// This is where the INDEX MATCH happens
// For each row in ledger
ledgerValues.forEach(ledgerRow => {
// Get the second column (index 1)
let item = ledgerRow[1];
// Initialize the column
let value = [];
// For each row in the source
sourceValues.some(sourceRow => {
// Check if the item is there
if (item == sourceRow[0]) {
// if so, add to value
value.push(sourceRow[1]);
// stop looking for values
return true
// if not matched, keep looking
} else return false
})
// Add the found value (or blank if not found)
// to the output array.
output.push(value);
})
return output;
}
Which can be used like this:
Whats nice about Apps Script is that you can customize it to your heart's content. In this example, the function automatically detects the height of the respective tables, so you don't need to fiddle around with ranges.
You might want to extend this function with arguments so that its more flexible. Or you could just have a few versions of it for different operations, if you don't have too many. Or refactor it... its up to you.
References
Apps Script
Custom Functions
Tutorials
SpreadsheetApp
use:
=ARRAYFORMULA(IFNA(VLOOKUP(C2:C, Items!E2:F, 2, 0)))

Need to manipulate results of line item with a code step in Zapier

Screenshot of my sample
I have a python code step that returns a line item object with the names of some components, their component IDs and the allocation of the components (see screenshot)
Now I need to return ONLY the set of values where the component_allocation is greater than 1 and the name of the component contains "User"
In other words, for the data in the picture I want to return:
Component Name: Additional User, Enterprise, annual
Component Allocation: 5
Component ID: 587086
Are my commas in the component name going to make this impossible? I'm not great at coding (wild understatement) and I don't understand how line items work very well. I can ALMOST do this with a spreadsheet style formula step instead of a code step because it has line item support, but no FIND or SEARCH function to match the word "user"
I've run into this before. Luckily, you have control of the return in your Step 3 code step. I tackled it by:
Return a list of JSON strings rather than (or together with) a list of objects. Your return would look like:
[
'{"component_allocation": 0, "component_name": "Addition user, monthly", "component_id": 565257}',
'{"component_allocation": 5, "component_name": "Addition user, yearly", "component_id": 565258}',
'{"component_allocation": 25, "component_name": "Addition user, biennual", "component_id": 565259}'
]
Convert the JSON string to an object in your new code step.
Run you normal code now

Can I pull a list of info out of an email?

I get a daily email that lists upcoming appointments, and their length. The number of appointments vary from day to day.
The emails go like this:
================
Today's Schedule
9:30 AM
3h
Brazilian Blowout
[Client #1 name]
12:30 PM
1h
Women's Cut
[Client 2 name]
6:00 PM
45m
Men's Cut
[Client #3 name]
Projected Revenue
===================
I want to create an event in a Google Calendar for each appointment, and it seems like zapier MIGHT be able to do this, but all the help resources I can find are very general in nature.
Is this do-able on Zapier? If so, any nudges in the right direction would be awesome.
Any thoughts greatly appreciated.
I had some time to kill and enjoy the odd challenge. So I have put together a solution that should do what you are looking for. I will break it down by steps.
TEMPLATE
Zapier Trigger - Step 1
Type: Trigger
Module: Gmail
Criteria: User Dependent
Comments: For the trigger zap you will want to use a Gmail specific trigger, something to the effect of "execute trigger on emails titled 'xyz'", or "emails labeled 'xyz'" if you setup a filter in your inbox.
Input screenshot:
Output Screenshot:
Zapier Action - Step 2
Type: Action
Module: Code (Python 3)
Comments: The Code offered by Zapier executes whatever (properly written) code you place in its container. It is especially handy as it allows you to incorporate data from previous steps in it through the use of a dictionary variable titled 'input_data'. Zapier offers the Code module in two languages: Javascript and Python. As I am most familiar with Python my solution for this step was written in Python. I will append the code to the end of this answer. Using the data held in the body of the email (retrieved in step 1) we can execute some string manipulations and datetime conversions to break apart the email into its component parts and pass those on to the following Action Step: Create Calendar Event.
Input Screenshot:
Output Screenshot:
Zapier Action - Step 3
Type: Action
Module: Google Calendar - Create Event
Comments: Using the data outputted from the previous code step we can fill out the required fields for creating a new appointment.
Input Screenshot:
Output Screenshot:
PYTHON CODE
from datetime import timedelta, date, datetime
'''
Goal: Extract individual appointment details from variable length email
Steps:
Remove all extraneous and new line characters.
Isolate each individual appointment and group its relevant details.
Derive appointment start and end times using appointment time and duration.
Return all appointments in a list.
'''
def format_appt_times(appt_dict):
appt_start_str = appt_dict.get("appt_start")
appt_dur_str = appt_dict.get("appt_length")
# isolate hour and minutes from appointment time
appt_s_hour = int(appt_start_str[:appt_start_str.find(":")])
if ("pm" in appt_start_str.lower()):
appt_s_hour = 12 if appt_s_hour + 12 >= 24 else appt_s_hour + 12
appt_s_min = int(appt_start_str[appt_start_str.find(":") + 1 :
appt_start_str.find(":") + 3])
# isolate hour and minutes from duration time
appt_d_hour = 0
appt_d_min = 0
if ("h" in appt_dur_str):
appt_d_hour = int(appt_dur_str[:appt_dur_str.find("h")])
if ("m" in appt_dur_str):
appt_d_min = int(appt_dur_str[appt_dur_str.find("m") - 2 : appt_dur_str.find("m")])
# NOTE: adjust timedelta hours depending on your relation to UTC
# create datetime objects for appointment start and end times
time_zone = timedelta(hours=0)
tdy = date.today() - time_zone
duration = timedelta(hours=appt_d_hour, minutes=appt_d_min)
appt_start_dto = datetime(year=tdy.year,
month=tdy.month,
day=tdy.day,
hour=appt_s_hour,
minute=appt_s_min)
appt_end_dto = appt_start_dto + duration
# return properly formatted datetime as string for use in next step.
return (appt_start_dto.strftime("%Y-%m-%dT%H:%M"),
appt_end_dto.strftime("%Y-%m-%dT%H:%M"))
def partition_list(target, part_size):
for data in range(0, len(target), part_size):
yield target[data : data + part_size]
def main():
# Remove all extraneous and new line characters.
email_body = input_data.get("email_body")
head,delin,*email_body,delin,foot = [text for text in email_body.splitlines() if text != ""]
appointment_list = []
# Isolate each individual appointment and group its relevant details.
for text in partition_list(email_body, 4):
template = {
"appt_start" : text[0],
"appt_end" : None,
"appt_length" : text[1],
"appt_title" : text[2],
"appt_client" : text[3]
}
appointment_list.append(template)
for appt in appointment_list:
appt["appt_start"], appt["appt_end"] = format_appt_times(appt)
return appointment_list
return main()
I am not sure of your familiarity with Python, or programming more generally, but the comments in the code explain what each section is doing. If you have any specific questions regarding aspects of the code let me know. Assuming your email template does not change this setup should work exactly as needed. Let me know if anything is unclear.
UPDATE
I thought it best to address your question in the original answer should anyone else have similar questions.
explaining how this code is removing the extra characters:
There is actually a fair bit going on in the first line, so I will do my best to break it down, and provide resources where necessary.
The code in question:
head,delin,*email_body,delin,foot = [text for text in email_body.splitlines() if text != ""]
First step here was to break the text into manageable chunks. I did so with the line email_body.splitlines() which, by default, breaks strings into a list at each newline character found (you can specify your own delimiter).
If we were to inspect the list at this moment its contents would be something of the following:
["================", "", "Today's Schedule", "", "9:30 AM", "", "3h", ..., "[Client #3 name]", "", "Projected Revenue", "", "==================="]
You will notice there is a fair amount of information in there that we really don't want.
First lets look at the "" elements. These are left over as a result of the blank lines between each line of text, which even though they are blank do still have newline characters at the end of them. There a number of ways you could address this within python. We could simply write a for-loop to go through and copy all elements that are not "" to a new list.
To me this felt like additional work, and besides, Python offers list comprehension for just such a scenario. I won't go too deep into list comprehension as there is a lot that can be said about it, and in more insightful ways than I could muster, but it essentially allows you to provide logic against a set of 'data' to form a list. In this case, I specifically wanted to filter out the "" elements returned from the call to splitlines().
And so you will see I address this with the following line
[text for text in email_body.splitlines() if text != ""]
With that we have a list as above less the "" elements. Now we must turn our attention towards the more 'dynamic' garbage strings. Again there are a number of ways to do this. A, not particularly flexible, option could be to simply store the strings we want to remove in variables something to the effect of:
garb_1 = "==================="
garb_2 = "Projected Revenue"
garb_3 = ...
and once again filter the list with yet another for-loop. I instead chose to leverage Python's list unpacking idiom. Which allows us to 'unpack' list objects (and I believe tuples) into variables. As an example:
one, two, three = ["a", "b", "c"]
I'm sure you can guess what is happening above, as long as we provide the same number of variables as are in the list we can 'unpack' it in this fashion. But wait! In our case we don't know how long the list is going to be as it is entirely dependent on the number of appointments you have for any given day. Well this is where star unpacking enters to elevate the functionality. Using my code as the example:
head,delin,*email_body,delin,foot = [text for text in email_body.splitlines() if text != ""]
The *, in plain-English, is saying "I don't know how many elements to expect just give me all of them in a list". As we know that there will always be two lines of garbage at the beginning and end of the email we can assign them to throw away variables and capture everything in between using our variable length *email_body container.
With all of this complete we now have a list with only the data we are looking to capture. If, as you say, there are additional lines of garbage before or after the email_body, you can simply add additional throw away variables to account for them.
Once again feel free to ask any follow up questions.
Michael
Resources
List Comprehension
Star Unpacking

App Inventor Fusion Table Column calling

I developed an app based on App Inventor and Fusion Tables. When I want to update total money by adding some money to already existing money it is giving some error.
When I use SELECT command to get information from fusion table it is taking then number with column Name. When i am trying to add both of them it is giving following error.
Error message
The result from a fusiontable includes always the header row...
from your example SELECT statement the result is
TotalPaid
5000
Obviously to add any value to that result will result in an error, because you only can add numeric values...
You first have to extract the value (in your example 5000) from the result. Convert the result into a list using the split block, just split at \n (new value) to get a list, then select the 2nd item using the select list item block.
Note: to be able to update something in a table, you need the ROWID, see also the SQL Reference Documentation of the Fusion Tables API.
For UPDATE statements the first step to be done is to get the ROWID of the row to be updated with a SELECT statement. The second step is to do the UPDATE.

Unique identifier script with Utilities.formatString() only when a new line is added

I'm trying to implement a unique identifier in column A. For this, I'm leaning towards the solution which has been discussed here: Auto incrementing Job Reference (using the Utilities.formatString() in Google sheets).
For ease of reading, here is my current script (I made some slight alterations to the original script):
function Identifier(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName("Epics");
var startcell = sh.getRange('A2').getValue();
var colValues = sh.getRange('A2:A').getValues();// get all the values in column A in an array
var max=0;// define the max variable to a minimal value
for(var r in colValues){ // iterate the array
var vv=colValues[r][0].toString().replace(/[^0-9]/g,'');// remove the letters from the string to convert to number
if(Number(vv)>max){max=vv};// get the highest numeric value in the column, no matter what happens in the column... this runs at array level so it is very fast
}
max++ ; // increment to be 1 above max value
sh.getRange(sh.getLastRow(), 1).setValue(Utilities.formatString('E%04d',max));// and write it back to sheet's last row.
}
The original function is designed to work with trigger 'On form submit' but I need this logic to be triggered only when a new line is added to my sheet. Trigger 'On edit' won't work because with that, every change to the sheet results in the last ID to get overwritten with the next new ID.
How can I make sure that my function is only called when a new line is added to the Googlesheet?
Edit [20-Jan.-2015]
Meanwhile I have adapted the script somewhat:
function Identifier(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName("Epics");
var r = ss.getActiveRange();
var editRow = parseInt(r.getRow()); // get the row the edit happend on
var lastRow = ss.getLastRow();
var startcell = sh.getRange('A2').getValue();
var colValues = sh.getRange('A2:A').getValues();// get all the values in column A in an array
var max=0;// define the max variable to a minimal value
if(lastRow == editRow){
for(var r in colValues){ // iterate the array
var vv=colValues[r][0].toString().replace(/[^0-9]/g,'');// remove the letters from the string to convert to number
if(Number(vv)>max){max=vv};// get the highest numeric value in the column, no matter what happens in the column... this runs at array level so it is very fast
}
max++ ; // increment to be 1 above max value
sh.getRange(sh.getLastRow(), 1).setValue(Utilities.formatString('E%04d',max));// and write it back to sheet's last row.
}
}
This almost works as intended. Still to solve:
With above script, the last ID-Field gets updated with each update to the last line of the sheet. I need an adaptation to above script so that updates in the last line do not result in new ID's. (must have)
When inserting a new row in an existing range, this is currently not recognised by the script. (nice to have)
I have been researching something along these lines and I can help you with part of your problem. You can create an installed trigger, and use the OnChange event to trigger when a row is inserted.
function myFunction(event){
if(event.changeType=='INSERT_ROW'){
// do Something
}
}
I am still trying to find an answer for the getting the inserted row's location.

Resources