Error when trying to set Google Forms quiz score - google-apps-script

I'm trying to change the grade of a response based on its answer.
Here's the code I'm using:
function myFunction() {
var form = FormApp.openById('formID123456');
// For a question with options: "1", "2", "3", and "4",
// award points for responses that correlate with their answers.
var formResponses = FormApp.getActiveForm().getResponses();
// Go through each form response
for (var i = 0; i < formResponses.length; i++) {
var response = formResponses[i];
var items = FormApp.getActiveForm().getItems();
// Assume it's the first item
var item = items[0];
var itemResponse = response.getGradableResponseForItem(item);
// Give 4 points for "4".
if (itemResponse != null && itemResponse.getResponse() == '4') {
var points = item.asScaleItem().getPoints();
itemResponse.setScore(points == 4);
}
// Give 3 points for "3".
else if (itemResponse != null && itemResponse.getResponse() == '3') {
var points = item.asScaleItem().getPoints();
itemResponse.setScore(points == 3);
}
// Give 2 points for "2".
else if (itemResponse != null && itemResponse.getResponse() == '2') {
var points = item.asScaleItem().getPoints();
itemResponse.setScore(points == 2);
}
// Give 1 points for "1".
else if (itemResponse != null && itemResponse.getResponse() == '1') {
var points = item.asScaleItem().getPoints();
itemResponse.setScore(points == 1);
// This saves the grade, but does not submit to Forms yet.
response.withItemGrade(itemResponse);
}
}
// Grades are actually submitted to Forms here.
FormApp.getActiveForm().submitGrades(formResponses);
}
This returns the error:
We're sorry, a server error occurred. Please wait a bit and try again. (line 23, file "Code")
It seemed like it was having issues changing the score of the response, but it didn't return a specific error, so I tried to isolate the part that changes the score.
Here, the script attempts only to change the score of the response.
function myFunction() {
var form = FormApp.openById('formID123456');
var formResponses = FormApp.getActiveForm().getResponses();
// Go through each form response
for (var i = 0; i < formResponses.length; i++) {
var response = formResponses[i];
var items = FormApp.getActiveForm().getItems();
// Assume it's the first item
var item = items[0];
var itemResponse = response.getGradableResponseForItem(item);
// Set Score to 3
var points = item.asScaleItem().getPoints();
itemResponse.setScore(points == 3);
}}
Again, it returned the same error, which confirms my suspicions. Why am I having this problem and how can I fix it? Any help would be much appreciated. Thanks!

As I mentioned in comments, your posted code erroneously uses a Boolean value in the call to ItemResponse#setScore, when the method expects to receive an Integer value.
Resolving the internal server error can be done by changing your entire if-elseif chain from this:
if (itemResponse != null && itemResponse.getResponse() == '4') {
var points = item.asScaleItem().getPoints();
itemResponse.setScore(points == 4); //<--- 'points == 4' evaluates to True or False
}
// Give 3 points for "3".
else if (...
to this:
// Skip changing the score if there was no answer or the answer is "falsey"
if (!itemResponse || !itemResponse.getResponse())
continue;
var answer = itemResponse.getResponse();
var newPoints = answer *1; // Convert "2" to 2, etc.
// Assumption: newPoints <= maximum possible points.
itemResponse.setScore(newPoints);
response.withItemGrade(itemResponse);
The below code is an example of how to set all graded items in all responses to a form to their maximum possible value.
function everyonePassesForTrying() {
var form = FormApp.getActiveForm();
var responses = form.getResponses();
responses.forEach(function (fr) {
fr.getGradableItemResponses().forEach(function (gr) {
if (gr.getResponse()) {
var maxPoints = getPointValue_(gr.getItem());
if (gr.getScore() !== maxPoints) {
// Re-grade the item's response.
gr.setScore(maxPoints);
// Update the form response with the new grade.
fr.withItemGrade(gr);
}
}
else { /* They didn't even try, so no change */ }
});
});
// Submit the altered scores.
form.submitGrades(responses);
}
var itemCache = {};
function getPointValue_(item) {
var id = item.getId();
// Use a pseudo-cache of the item's point values to avoid constantly determining what
// type it is, casting to that type, and getting the max points.
if(!itemCache[id]) {
// Haven't seen it yet, so cast and cache.
item = castToType_(item);
itemCache[id] = {maxPoints: item.getPoints() *1};
}
return itemCache[id].maxPoints;
}
function castToType_(item) {
// Cast the generic item to its type.
var CHECKBOX = FormApp.ItemType.CHECKBOX,
DATE = FormApp.ItemType.DATE,
DATETIME = FormApp.ItemType.DATETIME,
DURATION = FormApp.ItemType.DURATION,
LIST = FormApp.ItemType.LIST,
MULTICHOICE = FormApp.ItemType.MULTIPLE_CHOICE,
PARAGRAPH = FormApp.ItemType.PARAGRAPH_TEXT,
SCALE = FormApp.ItemType.SCALE,
TEXT = FormApp.ItemType.TEXT,
TIME = FormApp.ItemType.TIME;
switch (item.getType()) {
case CHECKBOX: item = item.asCheckboxItem();
break;
case DATE: item = item.asDateItem();
break;
case DATETIME: item = item.asDateTimeItem();
break;
case DURATION: item = item.asDurationItem();
break;
case LIST: item = item.asListItem();
break;
case MULTICHOICE: item = item.asMultipleChoiceItem();
break;
case PARAGRAPH: item = item.asParagraphTextItem();
break;
case SCALE: item = item.asScaleItem();
break;
case TEXT: item = item.asTextItem();
break;
case TIME: item = item.asTimeItem();
break;
default:
throw new Error("Unhandled gradable item type '" + item.getType() + "'");
break;
}
return item;
}

Related

Google Apps Script: insert and update texts by ID/name in Google Docs

I want to be able to insert, in Google Docs using Google Apps Script, custom texts with a given ID, so that afterwards I'd be able to update them (any number of times). The insertion should work with cursor placement as well as with replacing any selected elements.
I have a code that works pretty well for this (based partly on this answer), see below. I use "named ranges" for IDing the inserted/updated texts. The only problem is, when I have several such inserted texts immediately next to each other, and I update both repeatedly, suddenly the preceding one "absorbs" the following one (i.e., deletes it). So clearly it is a problem of the named ranges somehow expanding into each other, but I cannot figure out why.
// function for inserting text
insertAny = (textToInsert, textName = null, range = null) => {
var doc = DocumentApp.getActiveDocument();
var cursor = doc.getCursor();
var rangeBuilder = null;
if (cursor && (range === null)) {
// Attempt to insert text at the cursor position. If the insertion returns null, the cursor's
// containing element doesn't allow insertions, so show the user an error message.
var cElement = cursor.insertText(textToInsert);
if (!cElement) {
textName = null
DocumentApp.getUi().alert('Cannot insert text here.');
} else {
rangeBuilder = doc.newRange();
rangeBuilder.addElement(cElement);
}
} else {
var selection;
if (range === null) {
selection = DocumentApp.getActiveDocument().getSelection();
} else {
selection = range;
}
if (!selection) {
textName = null
DocumentApp.getUi().alert('Insertion omitted: A cursor placed in the text or a selected text is needed to indicate the position of the insertion.');
} else {
var elements = selection.getRangeElements();
var replace = true;
for (var i = 0; i < elements.length; i++) {
if (elements[i].isPartial()) {
var tElement = elements[i].getElement().asText();
var startIndex = elements[i].getStartOffset();
var endIndex = elements[i].getEndOffsetInclusive();
var text = tElement.getText().substring(startIndex, endIndex + 1);
tElement.deleteText(startIndex, endIndex);
if (replace) {
tElement.insertText(startIndex, textToInsert);
if (rangeBuilder === null) {
rangeBuilder = doc.newRange();
rangeBuilder.addElement(tElement, startIndex, startIndex + textToInsert.length - 1);
}
replace = false;
}
} else {
var eElement = elements[i].getElement();
// if not specified as "any", throws type errors for some reason
if (replace && eElement.editAsText) {
eElement.clear().asText().setText(textToInsert);
replace = false;
if (rangeBuilder === null) {
rangeBuilder = doc.newRange();
rangeBuilder.addElement(eElement);
}
} else {
if (replace && i === elements.length - 1) {
var parent = eElement.getParent();
parent[parent.insertText ? 'insertText' : 'insertParagraph'](parent.getChildIndex(eElement), textToInsert);
if (rangeBuilder === null) {
rangeBuilder = doc.newRange();
rangeBuilder.addElement(eElement);
}
replace = false; //not really necessary since it's the last one
}
eElement.removeFromParent();
}
}
}
}
}
if (textName !== null && rangeBuilder !== null) {
doc.addNamedRange(textName, rangeBuilder.build());
}
}
// function for updating text
const updateNamedRange = (textName, newText) => {
var doc = DocumentApp.getActiveDocument();
var myNamedRanges = doc.getNamedRanges(textName);
for (var i = 0; i < myNamedRanges.length; i++) {
var range = myNamedRanges[i].getRange();
insertAny(newText, textName, range);
}
}
Any ideas (or better solutions)?
Okay, so it seems that the reason is that if I insert text immediately next to a named range, it will automatically belong to that range. (Hence subsequent updates affected these unrelated parts too.)
My really hacky solution is to temporarily insert a placeholder character to separate the new text from any potential named ranges... It makes me laugh, but nothing else I tried works as well. This seems to be robust to all the tricky scenarios I can think of. My final code is below.
const insertAny = (textToInsert, textName = null, range = null) => {
var doc = DocumentApp.getActiveDocument();
var cursor = doc.getCursor();
var rangeBuilder = null;
if (cursor && (range === null)) {
// Attempt to insert text at the cursor position. If the insertion returns null, the cursor's
// containing element doesn't allow insertions, so show the user an error message.
var cElement = cursor.insertText(textToInsert);
if (!cElement) {
textName = null
DocumentApp.getUi().alert('Cannot insert text here.');
} else {
rangeBuilder = doc.newRange();
rangeBuilder.addElement(cElement);
}
} else {
var selection;
if (range === null) {
selection = DocumentApp.getActiveDocument().getSelection();
} else {
selection = range;
}
if (!selection) {
textName = null
DocumentApp.getUi().alert('Insertion omitted: A cursor placed in the text or a selected text is needed to indicate the position of the insertion.');
} else {
var elements = selection.getRangeElements();
if (range !== null) {
elements.length = 1;
}
var replace = true;
for (var i = 0; i < elements.length; i++) {
if (elements[i].isPartial()) {
var tElement = elements[i].getElement().asText();
var startIndex = elements[i].getStartOffset();
var endIndex = elements[i].getEndOffsetInclusive();
var text = tElement.getText().substring(startIndex, endIndex + 1);
if (replace) {
tElement.insertText(endIndex + 1, 'x');
tElement.deleteText(startIndex, endIndex);
tElement.insertText(startIndex + 1, textToInsert);
if (rangeBuilder === null) {
rangeBuilder = doc.newRange();
rangeBuilder.addElement(tElement, startIndex + 1, startIndex + 1 + textToInsert.length - 1);
}
replace = false;
tElement.deleteText(startIndex, startIndex);
} else {
tElement.deleteText(startIndex, endIndex);
}
} else {
var eElement = elements[i].getElement();
// if not specified as "any", throws type errors for some reason
if (replace && eElement.editAsText) {
eElement.clear().asText().setText(textToInsert);
replace = false;
if (rangeBuilder === null) {
rangeBuilder = doc.newRange();
rangeBuilder.addElement(eElement);
}
} else {
if (replace && i === elements.length - 1) {
var parent = eElement.getParent();
parent[parent.insertText ? 'insertText' : 'insertParagraph'](parent.getChildIndex(eElement), textToInsert);
if (rangeBuilder === null) {
rangeBuilder = doc.newRange();
rangeBuilder.addElement(eElement);
}
replace = false; //not really necessary since it's the last one
}
eElement.removeFromParent();
}
}
}
}
}
if (textName !== null && rangeBuilder !== null) {
doc.addNamedRange(textName, rangeBuilder.build());
}
}
const updateNamedRange = (textName, newText) => {
var doc = DocumentApp.getActiveDocument();
var myNamedRanges = doc.getNamedRanges(textName);
for (var i = 0; i < myNamedRanges.length; i++) {
var range = myNamedRanges[i].getRange();
myNamedRanges[i].remove();
insertAny(newText, textName, range);
}
}

retrieving EntryID for Checkbox item in Google Sheets Form not working

I used the code from #contributorpw on this post get Entry ID which is used to pre-populate fields (Items) in a Google Form URL and added the extended list of form types from #SourceFli (in same post).
I get the error message: "Exception: The parameters (String) don't match the method signature for FormApp.CheckboxItem.createResponse". That checkbox has only 1 option: "yes".
The rest of all my form items are only TEXT items and work fine.
function getPreFillEntriesMap(){
var ssOrder = SpreadsheetApp.openById(ORDER_SPREADSHEET_ID);
var orderFormUrl = ssOrder.getFormUrl();
var orderForm = FormApp.openByUrl(orderFormUrl);
var form = orderForm;
// var form = FormApp.openById(id);
var items = form.getItems();
var newFormResponse = form.createResponse();
var itms = [];
for(var i = 0; i < items.length; i++){
var response = getDefaultItemResponse_(items[i]);
if(response){
newFormResponse.withItemResponse(response);
itms.push({
id: items[i].getId(),
entry: null,
title: items[i].getTitle(),
type: "" + items[i].getType()
});
}
}
var ens = newFormResponse.toPrefilledUrl().split("&entry.").map(function(s){
return s.split("=")[0];
});
ens.shift();
return Logger.log(itms.map(function(r, i){
r.entry = this[i];
return r;
}, ens));
}
function getDefaultItemResponse_(item){
switch(item.getType()){
case FormApp.ItemType.TEXT:
return item.asTextItem().createResponse("1");
break;
case FormApp.ItemType.MULTIPLE_CHOICE:
return item.asMultipleChoiceItem()
.createResponse(item.asMultipleChoiceItem().getChoices()[0].getValue());
break;
case FormApp.ItemType.CHECKBOX:
return item.asCheckboxItem()
.createResponse(item.asCheckboxItem().getChoices()[0].getValue());
break;
case FormApp.ItemType.DATETIME:
return item.asDateTimeItem()
.createResponse(new Date());
break;
case FormApp.ItemType.DATE:
return item.asDateItem()
.createResponse(new Date());
break;
case FormApp.ItemType.LIST:
return item.asListItem()
.createResponse(item.asListItem().getChoices()[0].getValue());
break;
case FormApp.ItemType.PARAGRAPH_TEXT:
return item.asParagraphTextItem()
.createResponse(item.asParagraphTextItem().createResponse("some paragraph"));
break;
case FormApp.ItemType.CHECKBOX_GRID:
return item.asCheckboxGridItem()
.createResponse(item.asCheckboxGridItem().createResponse([item.asGridItem().getColumns[0], item.asGridItem().getRows[0]]));
break;
case FormApp.ItemType.DURATION:
return item.asDurationItem()
.createResponse(item.asDurationItem().createResponse(2, 20, 20));
break;
case FormApp.ItemType.GRID:
return item.asGridItem()
.createResponse(item.asGridItem().createResponse([item.asGridItem().getColumns[0], item.asGridItem().getRows[0]]));
break;
case FormApp.ItemType.SCALE:
return item.asScaleItem()
.createResponse(item.asScaleItem().createResponse(1));
break;
case FormApp.ItemType.TIME:
return item.asTimeItem()
.createResponse(item.asTimeItem().createResponse(1, 1));
break;
default:
return undefined;
}
}
response of createResponse(responses) of Class CheckboxItem is String[]. In your script, the string is used. I thought that this might be the reason of your issue. So how about the following modification?
From:
return item.asCheckboxItem()
.createResponse(item.asCheckboxItem().getChoices()[0].getValue());
To:
return item.asCheckboxItem()
.createResponse([item.asCheckboxItem().getChoices()[0].getValue()]);
Reference:
createResponse(responses)

Explanation for "need typeids" error

I have found a script on github to pull prices from the EVE-Central API to include in a Google Spreadsheet. I have uploaded that script into the editor and saved it. When I try to run it I get an error about a file or function that is missing.
need typeids (line 38, file 'Code')
When I try to use the function inside the spreadsheet it tells me the function does not exist. After a lot of reading I found out Google changed something in their script editors.
Here is the script I am using. And a picture of the error code I got.
/*
Takes a bunch of typeids from a list (duplicates are fine. multidimensional is fine) and returns a bunch of rows
with relevant price data.
TypeID,Buy Volume,Buy average,Buy max,Buy min,Buy Std deviation,Buy median,Buy Percentile,
Sell Volume,Sell Average,Sell Max,Sell Min,Sell std Deviation,Sell Median,sell Percentile
I'd suggest loading price data into a new sheet, then using vlookup to get the bits you care about in your main sheet.
loadRegionPrices defaults to the Forge
loadSystemPrices defaults to Jita
=loadRegionPrices(A1:A28)
=loadRegionPrices(A1:A28,10000002)
=loadRegionPrices(A1:A28,10000002,47)
=loadSystemPrices(A1:A28)
An example below:
https://docs.google.com/spreadsheets/d/1f9-4cb4Tx64Do-xmHhELSwZGahZ2mTTkV7mKDBRPrrY/edit?usp=sharing
*/
function loadRegionPrices(priceIDs,regionID,cachebuster){
if (typeof regionID == 'undefined'){
regionID=10000002;
}
if (typeof priceIDs == 'undefined'){
throw 'need typeids';
}
if (typeof cachebuster == 'undefined'){
cachebuster=1;
}
var prices = new Array();
var dirtyTypeIds = new Array();
var cleanTypeIds = new Array();
var url="http://api.eve-central.com/api/marketstat?cachebuster="+cachebuster+"&regionlimit="+regionID+"&typeid=";
priceIDs.forEach (function (row) {
row.forEach ( function (cell) {
if (typeof(cell) === 'number' ) {
dirtyTypeIds.push(cell);
}
});
});
cleanTypeIds = dirtyTypeIds.filter(function(v,i,a) {
return a.indexOf(v)===i;
});
var parameters = {method : "get", payload : ""};
var o,j,temparray,chunk = 100;
for (o=0,j=cleanTypeIds.length; o < j; o+=chunk) {
temparray = cleanTypeIds.slice(o,o+chunk);
var xmlFeed = UrlFetchApp.fetch(url+temparray.join("&typeid="), parameters).getContentText();
var xml = XmlService.parse(xmlFeed);
if(xml) {
var rows=xml.getRootElement().getChild("marketstat").getChildren("type");
for(var i = 0; i < rows.length; i++) {
var price=[parseInt(rows[i].getAttribute("id").getValue()),
parseInt(rows[i].getChild("buy").getChild("volume").getValue()),
parseFloat(rows[i].getChild("buy").getChild("avg").getValue()),
parseFloat(rows[i].getChild("buy").getChild("max").getValue()),
parseFloat(rows[i].getChild("buy").getChild("min").getValue()),
parseFloat(rows[i].getChild("buy").getChild("stddev").getValue()),
parseFloat(rows[i].getChild("buy").getChild("median").getValue()),
parseFloat(rows[i].getChild("buy").getChild("percentile").getValue()),
parseInt(rows[i].getChild("sell").getChild("volume").getValue()),
parseFloat(rows[i].getChild("sell").getChild("avg").getValue()),
parseFloat(rows[i].getChild("sell").getChild("max").getValue()),
parseFloat(rows[i].getChild("sell").getChild("min").getValue()),
parseFloat(rows[i].getChild("sell").getChild("stddev").getValue()),
parseFloat(rows[i].getChild("sell").getChild("median").getValue()),
parseFloat(rows[i].getChild("sell").getChild("percentile").getValue())];
prices.push(price);
}
}
}
return prices;
}
function loadSystemPrices(priceIDs,systemID,cachebuster){
if (typeof systemID == 'undefined'){
systemID=30000142;
}
if (typeof priceIDs == 'undefined'){
throw 'need typeids';
}
if (typeof cachebuster == 'undefined'){
cachebuster=1;
}
var prices = new Array();
var dirtyTypeIds = new Array();
var cleanTypeIds = new Array();
var url="http://api.eve-central.com/api/marketstat?cachebuster="+cachebuster+"&usesystem="+systemID+"&typeid=";
priceIDs.forEach (function (row) {
row.forEach ( function (cell) {
if (typeof(cell) === 'number' ) {
dirtyTypeIds.push(cell);
}
});
});
cleanTypeIds = dirtyTypeIds.filter(function(v,i,a) {
return a.indexOf(v)===i;
});
var parameters = {method : "get", payload : ""};
var o,j,temparray,chunk = 100;
for (o=0,j=cleanTypeIds.length; o < j; o+=chunk) {
temparray = cleanTypeIds.slice(o,o+chunk);
var xmlFeed = UrlFetchApp.fetch(url+temparray.join("&typeid="), parameters).getContentText();
var xml = XmlService.parse(xmlFeed);
if(xml) {
var rows=xml.getRootElement().getChild("marketstat").getChildren("type");
for(var i = 0; i < rows.length; i++) {
var price=[parseInt(rows[i].getAttribute("id").getValue()),
parseInt(rows[i].getChild("buy").getChild("volume").getValue()),
parseFloat(rows[i].getChild("buy").getChild("avg").getValue()),
parseFloat(rows[i].getChild("buy").getChild("max").getValue()),
parseFloat(rows[i].getChild("buy").getChild("min").getValue()),
parseFloat(rows[i].getChild("buy").getChild("stddev").getValue()),
parseFloat(rows[i].getChild("buy").getChild("median").getValue()),
parseFloat(rows[i].getChild("buy").getChild("percentile").getValue()),
parseInt(rows[i].getChild("sell").getChild("volume").getValue()),
parseFloat(rows[i].getChild("sell").getChild("avg").getValue()),
parseFloat(rows[i].getChild("sell").getChild("max").getValue()),
parseFloat(rows[i].getChild("sell").getChild("min").getValue()),
parseFloat(rows[i].getChild("sell").getChild("stddev").getValue()),
parseFloat(rows[i].getChild("sell").getChild("median").getValue()),
parseFloat(rows[i].getChild("sell").getChild("percentile").getValue())];
prices.push(price);
}
}
}
return prices;
}
The error message is very explicit. Here's the relevant code:
function loadSystemPrices(priceIDs,systemID,cachebuster){
if (typeof systemID == 'undefined'){
systemID=30000142;
}
if (typeof priceIDs == 'undefined'){
throw 'need typeids'; //// <<<< Line 38
}
Function loadSystemPrices() has been invoked with no value for the priceIDs parameter. This condition is explicitly checked by the code, and results in a custom error message being thrown on line 38.
That's happening because you've invoked the function from the debugger, with no parameters. You can work around this by writing a test function to pass parameters, as described in Debugging a custom function in Google Apps Script.

How do you shuffle values from ng-repeat? (AngularJS)

I tried this fiddle from this, this, this SO, and this.
I still can't make shuffle or random work on my end. My code has stopped here:
signupApp.filter('shuffle', function() {
var shuffledArr = [],
shuffledLength = 0;
return function(arr) {
if (!arr || !arr.length) { return; }
var o = arr.slice(0, arr.length);
if (shuffledLength == arr.length) return shuffledArr;
for(var j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
shuffledArr = o;
shuffledLength = o.length;
return arr.slice(o);
};
});
my ng-repeat
<ul ng-repeat="codes in response | shuffle">
<li ng-if="((codes.branch == formData.branches.alias) && (codes.taken == 0))">
<strong>{{codes.id}}</strong>
{{codes.code}}
</li>
</ul>
Results still do not randomize my outputs. Can someone help?
[UPDATE]: My code works in randomizing ng-repeat:
<span ng-repeat="codes in response">
<strong ng-if="((codes.branch == formData.branches.alias) && (codes.taken == 0))">
{{codes.code}}
</strong>
</span>
When limitTo:1 is added, it no longer displays anything. In my controller, the fisher-yates is run like this:
var shuffleArray = function(array) {
var m = array.length, t, i;
// While there remain elements to shuffle
while (m) {
// Pick a remaining element…
i = Math.floor(Math.random() * m--);
// And swap it with the current element.
t = array[m];
array[m] = array[i];
array[i] = t;
}
return array;
}
http.get("server/fetch.php").success(function(response){
scope.response = response;
shuffleArray(scope.response);
}).error(function() {
scope.response = "error in fetching data";
});
I can't figure out what's wrong with this. When limitTo: is set to 6, it outputs a single random value from the list. I also tried to set it to 10, and it outputs two random values from the list. I'm confused. I just want to limit it to 1 and randomize it.

Retrieving Postal Code with Google Maps Javascript API V3 Reverse Geocode

I'm trying to submit a query using the postal code to my DB whenever the googlemaps viewport center changes. I know that this can be done with reverse geocoding with something like:
google.maps.event.addListener(map, 'center_changed', function(){
newCenter();
});
...
function newCenter(){
var newc = map.getCenter();
geocoder.geocode({'latLng': newc}, function(results, status){
if (status == google.maps.GeocoderStatus.OK) {
var newzip = results[0].address_components['postal_code'];
}
});
};
Of course, this code doesn't actually work. So I was wondering how I would need to change this in order to extract the postal code from the results array.
Thanks
What I've realized so far is that in most cases the ZIPCODE is always the last value inside each returned address, so, if you want to retrieve the very first zipcode (this is my case), you can use the following approach:
var address = results[0].address_components;
var zipcode = address[address.length - 1].long_name;
You can do this pretty easily using the underscore.js libraray: http://documentcloud.github.com/underscore/#find
_.find(results[0].address_components, function (ac) { return ac.types[0] == 'postal_code' }).short_name
Using JQuery?
var searchAddressComponents = results[0].address_components,
searchPostalCode="";
$.each(searchAddressComponents, function(){
if(this.types[0]=="postal_code"){
searchPostalCode=this.short_name;
}
});
short_name or long_name will work above
the "searchPostalCode" var will contain the postal (zip?) code IF
and only IF you get one from the Google Maps API.
Sometimes you DO NOT get a "postal_code" in return for your query.
Alright, so I got it. The solution is a little uglier than I'd like, and I probably don't need the last for loop, but here's the code for anyone else who needs to extract crap from address_components[]. This is inside the geocoder callback function
// make sure to initialize i
for(i=0; i < results.length; i++){
for(var j=0;j < results[i].address_components.length; j++){
for(var k=0; k < results[i].address_components[j].types.length; k++){
if(results[i].address_components[j].types[k] == "postal_code"){
zipcode = results[i].address_components[j].short_name;
}
}
}
}
$.each(results[0].address_components,function(index,value){
if(value.types[0] === "postal_code"){
$('#postal_code').val(value.long_name);
}
});
You can also use JavaScript .find method which is similar to underscore _.find method but it is native and require no extra dependency.
const zip_code = results[0].address_components.find(addr => addr.types[0] === "postal_code").short_name;
This takes only two for loops. The "results" array gets updated once we found the first "type" to be "postal_code".
It then updates the original array with the newly found array set and loops again.
var i, j,
result, types;
// Loop through the Geocoder result set. Note that the results
// array will change as this loop can self iterate.
for (i = 0; i < results.length; i++) {
result = results[i];
types = result.types;
for (j = 0; j < types.length; j++) {
if (types[j] === 'postal_code') {
// If we haven't found the "long_name" property,
// then we need to take this object and iterate through
// it again by setting it to our master loops array and
// setting the index to -1
if (result.long_name === undefined) {
results = result.address_components;
i = -1;
}
// We've found it!
else {
postcode = result.long_name;
}
break;
}
}
}
You can also use this code, this function will help to get zip on button click or onblur or keyup or keydown.
Just pass the address to this function.
use google api with valid key and sensor option removed as it doesn't required now.
function callZipAPI(addSearchZip)
{
var geocoder = new google.maps.Geocoder();
var zipCode = null;
geocoder.geocode({ 'address': addSearchZip }, function (results, status) {
if (status == google.maps.GeocoderStatus.OK) {
//var latitude = results[0].geometry.location.lat();
//var longitude = results[0].geometry.location.lng();
var addressComponent = results[0].address_components;
for (var x = 0 ; x < addressComponent.length; x++) {
var chk = addressComponent[x];
if (chk.types[0] == 'postal_code') {
zipCode = chk.long_name;
}
}
if (zipCode) {
alert(zipCode);
}
else {
alert('No result found!!');
}
} else {
alert('Enter proper address!!');
}
});
}
I use this code to get "Postal code" and "locality", but you can use it to get any other field just changing the value of type:
JAVASCRIPT
var address = results[0].address_components;
var zipcode = '';
var locality = '';
for (var i = 0; i < address.length; i++) {
if (address[i].types.includes("postal_code")){ zipcode = address[i].short_name; }
if (address[i].types.includes("locality")){ locality = address[i].short_name; }
}
I think rather than depending on the index it better checks address type key inside the component. I solved this issue by using a switch case.
var address = '';
var pin = '';
var country = '';
var state = '';
var city = '';
var streetNumber = '';
var route ='';
var place = autocomplete.getPlace();
for (var i = 0; i < place.address_components.length; i++) {
var component = place.address_components[i];
var addressType = component.types[0];
switch (addressType) {
case 'street_number':
streetNumber = component.long_name;
break;
case 'route':
route = component.short_name;
break;
case 'locality':
city = component.long_name;
break;
case 'administrative_area_level_1':
state = component.long_name;
break;
case 'postal_code':
pin = component.long_name;
break;
case 'country':
country = component.long_name;
break;
}
}
places.getDetails( request_details, function(results_details, status){
// Check if the Service is OK
if (status == google.maps.places.PlacesServiceStatus.OK) {
places_postal = results_details.address_components
places_phone = results_details.formatted_phone_number
places_phone_int = results_details.international_phone_number
places_format_address = results_details.formatted_address
places_google_url = results_details.url
places_website = results_details.website
places_rating = results_details.rating
for (var i = 0; i < places_postal.length; i++ ) {
if (places_postal[i].types == "postal_code"){
console.log(places_postal[i].long_name)
}
}
}
});
This seems to work very well for me, this is with the new Google Maps API V3. If this helps anyone, write a comment, i'm writing my script as we speak... so it might change.
Using JSONPath, it's easily done with one line of code:
var zip = $.results[0].address_components[?(#.types=="postal_code")].long_name;
In PHP I use this code. Almost in every conditions it works.
$zip = $data["results"][3]["address_components"];
$zip = $index[0]["short_name"];
Romaine M. — thanks! If you just need to find the postal code in the first returned result from Google, you can do just 2 loops:
for(var j=0;j < results[0].address_components.length; j++){
for(var k=0; k < results[0].address_components[j].types.length; k++){
if(results[0].address_components[j].types[k] == "postal_code"){
zipcode = results[0].address_components[j].long_name;
}
}
}
In a word, that's a lot of effort. At least with the v2 API, I could retrieve those details thusly:
var place = response.Placemark[0];
var point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]);
myAddress = place.AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.Thoroughfare.ThoroughfareName
myCity = place.AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.LocalityName
myState = place.AddressDetails.Country.AdministrativeArea.AdministrativeAreaName
myZipCode = place.AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.PostalCode.PostalCodeNumber
There has got to be a more elegant way to retrieve individual address_components without going through the looping jujitsu you just went through.
This simple code works for me
for (var i = 0; i < address.length; i++) {
alert(address[i].types);
if (address[i].types == "postal_code")
$('#postalCode').val(address[i].long_name);
if (address[i].types == "")
$('#country').val(address[i].short_name);
}
Using Jquery
You cant be sure in which location in the address_components array the postal code is stored. Sometimes in address_components.length - 1 > pincode may not be there. This is true in "Address to latlng" geocoding.
You can be sure that Postal code will contain a "postal_code" string. So best way is to check for that.
var postalObject = $.grep(results[0].address_components, function(n, i) {
if (n.types[0] == "postal_code") {
return n;
} else {
return null;
}
});
$scope.query.Pincode = postalObject[0].long_name;
return $http.get('//maps.googleapis.com/maps/api/geocode/json', {
params: {
address: val,
sensor: false
}
}).then(function (response) {
var model= response.data.results.map(function (item) {
// return item.address_components[0].short_name;
var short_name;
var st= $.each(item.address_components, function (value, key) {
if (key.types[0] == "postal_code") {
short_name= key.short_name;
}
});
return short_name;
});
return model;
});
//autocomplete is the text box where u will get the suggestions for an address.
autocomplete.addListener('place_changed', function () {
//Place will get the selected place geocode and returns with the address
//and marker information.
var place = autocomplete.getPlace();
//To select just the zip code of complete address from marker, below loop //will help to find. Instead of y.long_name you can also use y.short_name.
var zipCode = null;
for (var x = 0 ; x < place.address_components.length; x++) {
var y = place.address_components[x];
if (y.types[0] == 'postal_code') {
zipCode = y.long_name;
}
}
});
It seems that nowadays it's better to get it from the restful API, simply try:
https://maps.googleapis.com/maps/api/geocode/json?latlng=40.714224,-73.961452&key=YOUR_KEY_HERE
Using an AJAX GET call works perfect!
Something like:
var your_api_key = "***";
var f_center_lat = 40.714224;
var f_center_lon = -73.961452;
$.ajax({ url: "https://maps.googleapis.com/maps/api/geocode/json?latlng="+f_center_lat+","+f_center_lon+"&key="+your_api_key,
method: "GET"
})
.done(function( res ) { if (debug) console.log("Ajax result:"); console.log(res);
var zipCode = null;
var addressComponent = res.results[0].address_components;
for (var x = 0 ; x < addressComponent.length; x++) {
var chk = addressComponent[x];
if (chk.types[0] == 'postal_code') {
zipCode = chk.long_name;
}
}
if (zipCode) {
//alert(zipCode);
$(current_map_form + " #postalcode").val(zipCode);
}
else {
//alert('No result found!!');
if (debug) console.log("Zip/postal code not found for this map location.")
}
})
.fail(function( jqXHR, textStatus ) {
console.log( "Request failed (get postal code via geocoder rest api). Msg: " + textStatus );
});
As I got it zip is the last or the one that before last.
That why this is my solution
const getZip = function (arr) {
return (arr[arr.length - 1].types[0] === 'postal_code') ? arr[arr.length - 1].long_name : arr[arr.length - 2].long_name;
};
const zip = getZip(place.address_components);
i think this is the most accurate solution:
zipCode: result.address_components.find(item => item.types[0] === 'postal_code').long_name;
Just search for postal_code in all types, and return when found.
const address_components = [{"long_name": "2b","short_name": "2b","types": ["street_number"]}, { "long_name": "Louis Schuermanstraat","short_name": "Louis Schuermanstraat", "types": ["route"]},{"long_name": "Gent","short_name": "Gent","types": ["locality","political" ]},{"long_name": "Oost-Vlaanderen","short_name": "OV","types": ["administrative_area_level_2","political"]},{"long_name": "Vlaanderen","short_name": "Vlaanderen","types": ["administrative_area_level_1","political"]},{"long_name": "België","short_name": "BE","types": ["country","political"]},{"long_name": "9040","short_name": "9040","types": ["postal_code"]}];
// address_components = results[0]address_components
console.log({
'object': getByGeoType(address_components),
'short_name': getByGeoType(address_components).short_name,
'long_name': getByGeoType(address_components).long_name,
'route': getByGeoType(address_components, ['route']).long_name,
'place': getByGeoType(address_components, ['locality', 'political']).long_name
});
function getByGeoType(components, type = ['postal_code']) {
let result = null;
$.each(components,
function() {
if (this.types.some(r => type.indexOf(r) >= 0)) {
result = this;
return false;
}
});
return result;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>