Importing Twilight Data to google sheets - json

I'm going to be honest, I know very little about any of this and the last time I did any form of programming was in high school 12 years ago.
I am needing to create a schedule for a low budget shoot, however, that is happening in August and I need to send out a daily schedule for the upcoming days as it changes.
I've been trying to work out how to potentially amend this so that it could include the other twilight times as well, but it keeps giving me an error:
// for an idiot, what am I doing wrong? Ideally it would be constructed in such a way that I can use it on future projects as well, placing a screen grab - it would be great if the formula can make reference to other cells and update as those cells update:
screengrab of google sheets
function SolarTimes(lat,long,date,type) {
var response = UrlFetchApp.fetch("https://api.sunrise-sunset.org/json?lat="+lat+"&lng="+long+"&date="+date);
var json = response.getContentText();
var data = JSON.parse(json);
var sunrise = data.results.sunrise;
var sunset = data.results.sunset;
var civil_dawn = data.results.civil_twilight_begin;
var civil_dusk = data.results.civil_twilight_end;
var nautical_dawn = data.results.nautical_twilight_begin;
var nautical_dusk = data.results.nautical_twilight_end;
var day_length = data.results.day_length;
{ if (type == "Sunrise")
return sunrise;
else if (type == "Sunset")
return sunset;
else if (type = "Civil_Dawn")
return civildawn;
else if (type == "Civil_Dusk")
return civildusk;
else if (type == "Nautical_Dawn")
return nauticaldawn;
else if (type == "Nautical_Dusk")
return nauticaldusk;
else
return day_length};
}

Here's an implementation that handles numeric dates and validates arguments.
/**
* Gets the sunrise or sunset time or day length at a location on a date.
*
* #param {36.7201600} latitude The north–south position to use.
* #param {-4.4203400} longitude The east-west position to use.
* #param {"sunrise"} type One of "all", "sunrise", "sunset", "civil_dawn", "civil_dusk", "nautical_dawn", "nautical_dusk" or "day_length".
* #param {D2} date Optional. Defaults to the current date.
* #return {String|String[][]} The requested time as a text string. With "all", an array of types and times.
* #customfunction
*/
function Daylight(latitude, longitude, type, date) {
// see https://stackoverflow.com/a/72675674/13045193
// note: api.sunrise-sunset.org/json does not handle polar night nor midnight sun correctly
'use strict';
const [lat, lng, key, dateString] = _validate(arguments);
const url = `https://api.sunrise-sunset.org/json?lat=${lat}&lng=${lng}&date=${dateString}`;
const { results, status } = JSON.parse(UrlFetchApp.fetch(url).getContentText());
if (key === 'all') {
return Object.keys(results).map(key => [key, results[key]]);
}
return results[key] ?? NaN;
/**
* Validates function arguments.
*/
function _validate(args) {
if (args.length < 3 || args.length > 4) {
throw new Error(`Wrong number of arguments to Daylight. Expected 3 or 4 arguments, but got ${args.length} arguments.`);
}
const lat = Number(latitude);
const lng = Number(longitude);
if (latitude === '' || Number.isNaN(lat) || lat > 90 || lat < -90 || longitude === '' || Number.isNaN(lng) || lng > 180 || lng < -180) {
throw new Error(`Daylight expected a numeric latitude [-90, 90] and longitude [-180, 180], but got the ${typeof latitude} '${latitude}' and the ${typeof longitude} '${longitude}'.`);
}
return [
lat,
lng,
type.toLowerCase().replace('dawn', 'twilight_begin').replace('dusk', 'twilight_end'),
_dateToISO8601(date),
];
}
/**
* Parses a date or string to an ISO8601 date string.
*/
function _dateToISO8601(date) {
if (date === '' || (date == null)) {
date = new Date();
}
if (typeof date === 'string') {
date = new Date(Date.parse(date));
}
if (Object.prototype.toString.call(date) !== '[object Date]') {
throw new Error(`Daylight expected a date, but '${date}' is a ${typeof date}.`);
}
return Utilities.formatDate(date, SpreadsheetApp.getActive().getSpreadsheetTimeZone(), 'yyyy-MM-dd');
}
}

The code looks more or less fine. But the cell contains the little error I think. It should be thusly:
=Solartimes($G$3,$H$3,text($B$6,"yyyy-mm-dd"),C6)
As for the code I'd propose to use switch/case for this case:
function SolarTimes(lat,long,date,type) {
var response = UrlFetchApp.fetch("https://api.sunrise-sunset.org/json?lat="+lat+"&lng="+long+"&date="+date);
var json = response.getContentText();
var {results} = JSON.parse(json);
switch (type.toLowerCase()) {
case ('sunrise'): return results.sunrise;
case ('sunset'): return results.sunset;
case ('civil_dawn'): return results.civil_twilight_begin;
case ('civil_dusk'): return results.civil_twilight_end;
case ('nautical_dawn'): return results.nautical_twilight_begin;
case ('nautical_dusk'): return results.nautical_twilight_end;
case ('day_length'): return results.day_length;
}
return '--';
}
It works about the the same way but looks cleaner.
Just in case. The line:
var {results} = JSON.parse(json);
is the same as:
var data = JSON.parse(json);
var results = data.results;
See:
Destructuring assignment

Related

Tweaking publicly-available Travel Time script to allow Waypoints

I am trying to add to a publicly-available script to give it a bit more functionality, but I keep getting an "invalid argument: address" error.
The original script, which does what it advertises:
/**
* Get Distance between 2 different addresses.
* #param start_address Address as string Ex. "300 N LaSalles St, Chicago, IL"
* #param end_address Address as string Ex. "900 N LaSalles St, Chicago, IL"
* #param return_type Return type as string Ex. "miles" or "kilometers" or "minutes" or "hours"
* #customfunction
*/
function GOOGLEMAPS(start_address,end_address,return_type) {
// https://www.chicagocomputerclasses.com/
// Nov 2017
// improvements needed
var mapObj = Maps.newDirectionFinder();
mapObj.setOrigin(start_address);
mapObj.setDestination(end_address);
var directions = mapObj.getDirections();
var getTheLeg = directions["routes"][0]["legs"][0];
var meters = getTheLeg["distance"]["value"];
switch(return_type){
case "miles":
return meters * 0.000621371;
break;
case "minutes":
// get duration in seconds
var duration = getTheLeg["duration"]["value"];
//convert to minutes and return
return duration / 60;
break;
case "hours":
// get duration in seconds
var duration = getTheLeg["duration"]["value"];
//convert to hours and return
return duration / 60 / 60;
break;
case "kilometers":
return meters / 1000;
break;
default:
return "Error: Wrong Unit Type";
}
}
My tweaked version, which simply adds the arguments for up to 4 waypoints and then adds them into the mapObj variable used to get the directions.
function GOOGLEMAPS(start_address,end_address, return_type, waypoint1, waypoint2, waypoint3, waypoint4) {
// https://www.chicagocomputerclasses.com/
// Nov 2017
// improvements needed
var mapObj = Maps.newDirectionFinder();
mapObj.setOrigin(start_address);
mapObj.setDestination(end_address);
if(waypoint1 !== null ){mapObj.addWaypoint(waypoint1);}
if(waypoint2 !== null ){mapObj.addWaypoint(waypoint2);}
if(waypoint3 !== null ){mapObj.addWaypoint(waypoint3);}
if(waypoint4 !== null ){mapObj.addWaypoint(waypoint4);}
var directions = mapObj.getDirections();
var getTheLeg = directions["routes"][0]["legs"][0];
var meters = getTheLeg["distance"]["value"];
switch(return_type){
case "miles":
return meters * 0.000621371;
break;
case "minutes":
// get duration in seconds
var duration = getTheLeg["duration"]["value"];
//convert to minutes and return
return duration / 60;
break;
case "hours":
// get duration in seconds
var duration = getTheLeg["duration"]["value"];
//convert to hours and return
return duration / 60 / 60;
break;
case "kilometers":
return meters / 1000;
break;
default:
return "Error: Wrong Unit Type";
}
}
The line giving the error is this added one. The if clause is my attempt at making the argument optional, as I'll be using this on lists of waypoints of varying length.
if(waypoint1 !== null ){mapObj.addWaypoint(waypoint1);}
For example, in your script, when the value of waypoint1 is not given as the argument of GOOGLEMAPS, the value becomes undefined. In this case, waypoint1 !== null is true. I thought that this might be the reason for your issue of invalid argument: address because in this case, mapObj.addWaypoint(waypoint1) is run with undefined.
If you want to run mapObj.addWaypoint(waypoint1) when waypoint1 has the value, how about the following modification?
From:
if(waypoint1 !== null ){mapObj.addWaypoint(waypoint1);}
if(waypoint2 !== null ){mapObj.addWaypoint(waypoint2);}
if(waypoint3 !== null ){mapObj.addWaypoint(waypoint3);}
if(waypoint4 !== null ){mapObj.addWaypoint(waypoint4);}
To:
if (waypoint1 !== undefined) { mapObj.addWaypoint(waypoint1); }
if (waypoint2 !== undefined) { mapObj.addWaypoint(waypoint2); }
if (waypoint3 !== undefined) { mapObj.addWaypoint(waypoint3); }
if (waypoint4 !== undefined) { mapObj.addWaypoint(waypoint4); }
or
if (waypoint1) { mapObj.addWaypoint(waypoint1); }
if (waypoint2) { mapObj.addWaypoint(waypoint2); }
if (waypoint3) { mapObj.addWaypoint(waypoint3); }
if (waypoint4) { mapObj.addWaypoint(waypoint4); }
References:
null
undefined

Assign JSON value to variable based on value of a different key

I have this function for extracting the timestamp from two JSON objects:
lineReader.on('line', function (line) {
var obj = JSON.parse(line);
if(obj.Event == "SparkListenerApplicationStart" || obj.Event == "SparkListenerApplicationEnd") {
console.log('Line from file:', obj.Timestamp);
}
});
The JSON comes from a log file(not JSON) where each line represents an entry in the log and each line also happens to be in JSON format on its own.
The two objects represent the start and finish of a job. These can be identified by the event key(SparkListenerApplicationStart and SparkListenerApplicationEnd). They also both contain a timestamp key. I want to subtract the end time from the start time to get the duration.
My thinking is to assign the timestamp from the JSON where Event key = SparkListenerApplicationStart to one variable and assign the timestamp from the JSON where Event key = SparkListenerApplicationEnd to another variable and subtract one from the other. How can I do this? I know I can't simply do anything like:
var startTime = if(obj.Event == "SparkListenerApplicationStart"){
return obj.Timestamp;
}
I'm not sure if I understood, but if are reading rows and want get the Timestamp of each row I would re-write a new object;
const collection = []
lineReader.on('line', function (line) {
var obj = JSON.parse(line);
if(obj.Event == "SparkListenerApplicationStart" || obj.Event == "SparkListenerApplicationEnd") {
// console.log('Line from file:', obj.Timestamp);
collection.push(obj.Timestamp)
}
});
console.log(collection);
Where collection could be a LocalStorage, Global Variable, or something alike.
Additional info
With regard to my comment where I queried how to identify the start and end times, I ended up setting start as the smallest value and end as the largest. Here is my final code:
const collection = []
lineReader.on('line', function (line) {
var obj = JSON.parse(line);
if((obj.Event == "SparkListenerApplicationStart" || obj.Event == "SparkListenerApplicationEnd")) {
collection.push(obj.Timestamp);
if(collection.length == 2){
startTime = Math.min.apply(null, collection);
finishTime = Math.max.apply(null, collection);
duration = finishTime - startTime;
console.log(duration);
}
}
});

How can I order my string in as3

A complex question :
I've got this code (not the complete code, but the essentials for the question, I think) :
var $pmm:String;
var $pms:String;
var $bmm:String;
var $bms:String;
function get haute1():String { return $pmm; };
function get haute2():String { return $pms; }
function get basse1():String { return $bmm; };
function get basse2():String { return $bms; };
accueil.todayHaute_txt.htmlText = haute1;
accueil.todayBasse_txt.htmlText = basse1;
accueil.todayHauteSecond_txt.htmlText = haute2;
accueil.todayBasseSecond_txt.htmlText = basse2;
"haute1" is an hour (in 24h format). Something like "13h25".
It changes everyday.
Question : How can put them in ascending order in AS3 ?
Example : If haute1 = 15h20, haute2= 6h00, basse1= 11h and basse2 = 17h, the function would put them in this order :
"haute2", then "basse1", then "haute1" and finally "basse2".
Thx
EDIT
I add this code that I have. is it helping you ?
/ Assigns hours and tidal heights
$pmm = convdateheure($tpbs[1 + $deltapm]);
$pms = convdateheure($tpbs[3 + $deltapm]);
$bmm = convdateheure($tpbs[2 - $deltapm]);
$bms = convdateheure($tpbs[4 - $deltapm]);
function convdateheure($valeur:Number):String
{
var $heure:Number = Math.floor($valeur);
var $minute:Number = Math.floor(Math.floor(($valeur - Math.floor($valeur)) * 100) * 0.6);
var hoursLabel:String = "", minsLabel:String = "";
if ($heure == 24) $heure = 0; // Check if at the 24 hour mark, change to 0
if ($heure < 10) hoursLabel += "0" + $heure.toString(); else hoursLabel = $heure.toString();
if ($minute < 10) minsLabel += "0" + $minute.toString(); else minsLabel = $minute.toString();
return hoursLabel + ":" + minsLabel;
}
If you want to order some dates written in some String format:
One way would be, depending on you date string format, just to push them into array and sort them as strings, then read them all.
Another way would be to first parse those strings into Date instances, and push their Date.time property to array, sort it, then do reverse: parse all time values from sorted array into new Date instances then use Date.toString or similar.
Assuming that $valuer is a numerical value:
var timesArray:Array = new Array();
var convertedTimesArray:Array = new Array();
function sortTimes():void{
timesArray.push($valuer);
timesArray.sort(Array.NUMERIC);
}
function convertTimes():void{
convertedTimesArray = []; // clear the array
for (var i:int = 0; i < timesArray.length; i++){
var s:String = convdateheure(timesArray[i]);
convertedTimesArray.push(s);
}
}
That should give you one array of actual times, sorted in numerical order, and one array sorted in the same numerical order, but converted to String values using your function.

Why can't Google Sheets find reverseGeocode method from Maps API?

I'm using a Google Sheets function to reverse geocode a list lat/long coordinates. It looks like this:
function getAdd(lat, lng) {
if (lat == "") {
return "You have to provide latitudinal coordinates to the place"
} if (lng == ""){
return "You have to provide longitudinal coordinates to the place"
}
var response = Maps.newGeocoder().reverseGeocode(lat, lng);
for (var i = 0; i < response.results.length; i++) {
var result = response.results[i];
Utilities.sleep(1000);
return result.formatted_address;
}
};
Question 1: Why is Google Sheets giving me the following error: "Cannot find method reverseGeocode(object,(class))"?
Question 2: Once I fix that, how can fetch country names from the result array instead of the complete address from the results?
You're trying to return a result for each result in the response object. Instead you have to choose one:
function getAdd(lat, lng) {
if (lat == "") {
return "You have to provide latitudinal coordinates to the place"
} if (lng == ""){
return "You have to provide longitudinal coordinates to the place"
}
var response = Maps.newGeocoder().reverseGeocode(lat, lng);
return response.results[0].formatted_address;
};
If you're looking for just the country, the format of the result object is here:
https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
In that case, you should iterate through the results[0] object and test to see if the types includes "Country". If it does, select the results[0].address_components[i].short_name where i is your iterator. Or use long_name instead.
Ok, figured it out. Here's how I eventually got country from lat/long:
function getAdd(lat, lng) {
if (lat == "") {
return "Insert latitude."
}
if (lng == ""){
return "Insert longitude."
}
var response = Maps.newGeocoder().reverseGeocode(lat,lng);
Utilities.sleep(1000); //in order not to exeed api calls per second
for (var i in response.results) {
var result = response.results[i];
}
for (var j in result.address_components) {
for (var k in result.address_components[j].types) {
if (result.address_components[j].types[k] == "country") {
return result.address_components[j].long_name;
}
}
}
};
Thank you for posting this it just helped me a lot with what I was trying to do. Just a quick note that your first loop simply returns the last record and not all data is in the last record but the country seems to always be there. If anyone (like me) is looking for locality names you can simply choose the first record and more data is available to you.
I just changed:
for (var i in response.results) {
var result = response.results[i];
}
to:
var result = response.results[0];
Full Code returns the locality and country:
function getAdd(lat, lng) {
var response = Maps.newGeocoder().reverseGeocode(lat,lng);
var country="";
var place="";
Utilities.sleep(1000); //in order not to exeed api calls per second
var result = response.results[0];
for (var j in result.address_components) {
for (var k in result.address_components[j].types) {
if (result.address_components[j].types[k] == "country") {
country = result.address_components[j].long_name;
}
if(result.address_components[j].types[k] == "locality") {
place = result.address_components[j].long_name;
}
}
}
return [place, country];
};

How can I use a custom function with FILTER?

I have a custom function defined that extracts part of an address from a string:
/*
* Return the number preceding 'N' in an address
* '445 N 400 E' => '445'
* '1083 E 500 N' => '500'
*/
function NorthAddress(address) {
if (!address) return null;
else {
var North = new RegExp('([0-9]+)[\\s]+N');
var match = address.match(North);
if (match && match.length >= 2) {
return match[1];
}
return null;
}
}
I want to use this function as one of the conditions in a call to FILTER(...) in the spreadsheet where I have these addresses stored:
=FILTER('Sheet 1'!A:A, NorthAddress('Sheet 1'!B:B) >= 450))
But when I call NorthAddress like this, it gets an array of all the values in column B and I can't for the life of me find any documentation as to how I need to handle that. The most obvious way (to me) doesn't seem to work: iterate over the array calling NorthAddress on each value, and return an array of the results.
What does my function need to return for FILTER to work as expected?
When a custom function is called passing a multi-cell range, it receives a matrix of values (2d array), it's doesn't matter if the range is a single column or a single row, it's always a matrix. And you should return a matrix as well.
Anyway, I would not use a custom function to this, as there is already the native spreadsheet formulas: RegexMatch, RegexExtract and RegexReplace formulas. To get the "if match" behavior, just wrap them in a IfError formula.
It doesn't work because address is, if you pass only one cell as arg a string, a range, a matrix of string.
So you return a string, FILTER use a boolean array to filter data, so the condition of your filter is string < number.
You just have to convert the string to a number when you returning a value
/*
* Return the number preceding 'N' in an address
* '445 N 400 E' => '445'
* '1083 E 500 N' => '500'
*/
function NorthAddress(address) {
if(typeof address == "string"){
if (!address) return "#N/A";
else {
var North = new RegExp('([0-9]+)[\\s]+N');
var match = address.match(North);
if (match && match.length >= 2) {
return parseInt(match[1]);
}
return "#N/A";
}
} else {
var matrix = new Array();
for(var i = 0; i<address.length; i++){
matrix[i] = new Array();
for(var j = 0; j<address[i].length; j++){
var North = new RegExp('([0-9]+)[\\s]+N');
var match = address[i][j].match(North);
if (match && match.length >= 2) {
matrix[i].push(parseInt(match[1]));
}
}
}
return matrix;
}
}
Hope this will help.
I will add this as an answer, because I found the custom function returns an error if numerical values are passed in the referenced cell or range when toString() is not invoked:
function NorthAddress(address) {
if (!address) return null;
else {
if (address.constructor == Array) {
var result = address;
}
else {
var result = [[address]];
}
var north = new RegExp('([0-9]+)[\\s]+N');
var match;
for (var i = 0; i < result.length; i++) {
for (var j = 0; j < result[0].length; j++) {
match = result[i][j].toString().match(north);
if (match && match.length >= 2) {
result[i][j] = parseInt(match[1]);
}
else {
result[i][j] = null;
}
}
}
return result;
}
}