Skip to content

Instantly share code, notes, and snippets.

@vadim0x60
Created December 4, 2025 22:39
Show Gist options
  • Select an option

  • Save vadim0x60/65ee294d4563b47b8c15f12b58e07dcb to your computer and use it in GitHub Desktop.

Select an option

Save vadim0x60/65ee294d4563b47b8c15f12b58e07dcb to your computer and use it in GitHub Desktop.
const hour_ms = 3600 * 1000
function average(arr) {
sum = null
for (const elem of arr) {
if (elem != null) {
sum += elem
}
}
if (sum != null) {
return sum / arr.length
}
else {
return null
}
}
function close_enough(warning, event) {
let too_late = warning['start'].getTime() - event.getEndTime().getTime() > event_spillover
let too_early = event.getStartTime().getTime() - warning['end'].getTime() > event_spillover
return !(too_late || too_early)
}
function time_filter(events, start, end) {
let filtered = []
for (const event of events) {
let startstamp = event['start'] ? event['start'].getTime() : Number.NEGATIVE_INFINITY
let endstamp = event['end'] ? event['end'].getTime() : Number.POSITIVE_INFINITY
startstamp = Math.max(start.getTime(), startstamp)
endstamp = Math.min(end.getTime(), endstamp)
if (endstamp >= startstamp) {
let copy = {...event}
copy['start'] = new Date(startstamp),
copy['end'] = new Date(endstamp)
filtered.push(copy)
}
}
return filtered
}
function event_time(event) {
return event.getStartTime().getTime() + event.getEndTime().getTime()
}
function sort_events(events) {
return events.sort((a, b) => (a['start'].getTime() + a['end'].getTime()) - (b['start'].getTime() + b['end'].getTime()))
}
function clear_calendar(calendar, start, end) {
for (let event of calendar.getEvents(start, end)) {
event.deleteEvent()
}
}
function populate_calendar(calendar, events) {
for (let event of events) {
if (event != null) {
calendar.createEvent(event['description'], event['start'], event['end'])
}
}
}
function provision_calendar(calendar_name) {
let calendars = CalendarApp.getCalendarsByName(calendar_name)
let the_calendar
if (calendars.length == 0) {
the_calendar = CalendarApp.createCalendar(calendar_name)
}
else {
the_calendar = calendars[0]
}
return the_calendar
}
function getDaysArray(start, end) {
days = []
d = new Date(start)
d.setDate(d.getDate() - 1)
days.push(new Date(d))
while(d <= end) {
d.setDate(d.getDate() + 1)
days.push(new Date(d))
}
return days
}
function zip_longest(args) {
result = []
length = Math.max(...args.map(a => a.length))
for (i = 0; i < length; i++) {
result.push(args.map(a => a[i] || null))
}
return result
}
function smart_append(events, event) {
if (event == null) return
latest_event = events.at(-1)
if (events.length > 0 && latest_event['description'] == event['description'] && latest_event['end'] >= event['start']) {
latest_event['start'] = new Date(Math.min(latest_event['start'], event['start']))
latest_event['end'] = new Date(Math.max(latest_event['end'], event['end']))
}
else {
events.push(event)
}
}
const loc_regex = new RegExp('\-?([0-9\.]+);\-?([0-9\.]+)')
const geocode_horizon = 5 // days
const geocoder = Maps.newGeocoder()
function geocode_location(location_description) {
let latlon, lat, lon
if (!location_description) {
return [null, null]
}
let match = loc_regex.exec(location_description)
if (match) {
[latlon, lat, lon] = match
}
if (lat && lon) {
return [lat, lon]
}
else {
console.log(location_description)
for (const result of geocoder.geocode(location_description).results) {
lat = result.geometry.location.lat
lon = result.geometry.location.lng
return [lat, lon]
}
}
return [null, null]
}
function geocode_events(recheck) {
let now = new Date()
let horizon_end = new Date()
horizon_end.setDate(now.getDate() + 5)
for (let calendar of CalendarApp.getAllOwnedCalendars()) {
for (let event of calendar.getEvents(now, horizon_end)) {
if (!recheck && event.getTag('lat') && event.getTag('lon')) {
continue
}
let [lat, lon] = geocode_location(event.getLocation())
if (lat && lon) {
event.setTag('lat', lat.toString())
event.setTag('lon', lon.toString())
}
}
}
}
function* all_locations(horizon_start, horizon_end) {
var warnings = []
var location_events = []
var locations_left = location_lookahead
for (calendar_name of location_calendars) {
calendar = CalendarApp.getCalendarsByName(calendar_name)[0]
location_events.push(...calendar.getEvents(horizon_start, horizon_end))
location_events = location_events.sort((a, b) => event_time(a) - event_time(b))
}
for (let location of location_events) {
if (location.getTag('lat') && location.getTag('lon')) {
yield location
locations_left -= 1
}
if (locations_left == 0) break
}
}
function assign_geocodes() {
geocode_events(false)
}
function review_geocodes() {
geocode_events(true)
}
const weather_api_key = 'nice try'
const weather_lookahead = 3 * 24 * 60 * 60 * 1000 // 3 days
const event_spillover = 4 * 60 * 60 * 1000 // 4 hours
// weather_requests_per_day = 800
const location_lookahead = 10
const weather_trigger_minutes = 15 //roundup((24 * 60 * location_lookahead) / (2 * weather_requests_per_day))
// My weather perferences
const too_hot = 27
const max_pop = 0.75 // pop - probability of precipitation
const temperature_format = new Intl.NumberFormat("en-GB", { style: "decimal", signDisplay: 'always' });
const weather_calendar = provision_calendar('#weather')
const location_calendars = ['uncommited', 'commited', 'crucial', 'log', 'stays']
function as_warning(forecast, duration, location) {
problem = ''
temperature = null
description = null
if ('feels_like' in forecast) {
temperature = Number(forecast['feels_like']) || average(Object.values(forecast['feels_like']))
if (temperature > too_hot) {
problem += 'hot'
}
}
pop = forecast['pop'] || (forecast['precipitation'] > 0)
if (pop > max_pop) {
problem += 'rain'
}
if (problem) {
if ('weather' in forecast) {
description = forecast['weather'][0]['description']
}
description = (description || problem) + ' @' + location
if (temperature) {
description = temperature_format.format(Math.round(temperature)) + ' ' + description
}
return {
'start': new Date(forecast.dt * 1000),
'end': new Date((forecast.dt + duration) * 1000),
'description': description
}
}
else {
return null
}
}
function fetch_local_warnings(lat, lon, loc) {
var warnings = []
const api_call = 'https://api.openweathermap.org/data/3.0/onecall?lat=' + lat + '&lon=' + lon + '&appid=' + weather_api_key + '&units=metric'
const weather = JSON.parse(UrlFetchApp.fetch(api_call));
if ('minutely' in weather) {
for (forecast of weather['minutely']) {
smart_append(warnings, as_warning(forecast, 60, loc))
}
}
for (forecast of weather['hourly'].slice(1)) {
smart_append(warnings, as_warning(forecast, 60 * 60, loc))
}
return warnings
}
function update_weather() {
now = new Date()
horizon_end = new Date(now.getTime() + weather_lookahead)
warnings = []
for (location of all_locations(now, horizon_end)) {
local_warnings = fetch_local_warnings(location.getTag('lat'), location.getTag('lon'), location.getTitle())
local_warnings = local_warnings.filter(w => w && close_enough(w, location))
warnings.push(...local_warnings)
}
now.setMinutes(0,0,0) // calendar.getEvents() filters by start of event instead of its full span
clear_calendar(weather_calendar, now, horizon_end)
populate_calendar(weather_calendar, warnings)
}
function deleteAllTriggers(){
let triggers = ScriptApp.getProjectTriggers();
for (let trigger of triggers) {
ScriptApp.deleteTrigger(trigger);
}
}
function install() {
//Delete any already existing triggers so we don't create excessive triggers
deleteAllTriggers();
//Schedule sync routine
ScriptApp.newTrigger("update_weather").timeBased().everyMinutes(weather_trigger_minutes).create()
ScriptApp.newTrigger("assign_geocodes").timeBased().everyMinutes(10).create()
ScriptApp.newTrigger("review_geocodes").timeBased().everyHours(12).create()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment