#!/usr/bin/lua --[[ foxclock version ᚢ.1 please read until the next 🐇 foxclock is a fuzzy clock for fuzzy aminals. it uses a heretical take on the ancient method of unequal, or seasonal, hours which was displaced by the unceasing tide of standardization, where the daylight and the nightdark are each divided into 7 sections (not the traditional 12), so that the length of a day hour and a night hour are only the same on equinoxes. summerday hours are long and lazy, winterday hours are fleeting and glittering. the non-suncalc parts of this software were written by someone and their friend, © a year of bnuuys. they are distributed under the Little Aminal Public License. you are free to use, redistribute, modify, destroy, love, hate, pet, kiss, or fuck the software ("the software") under the following conditia: - you agree ur cute soft little aminal in a big wide world - if ur plant/mycelium/rock/ocean/ghost/star/house/superorganism or something Else, that's cool too actually, don't worry about it. - u'll affirm your animality(/beingness) in some way today, such as by - sniffing at a berry - stretching your wings - feeling sunlight on your skin - feeling wind in your fur - feeling a tree looking back at you - you will help a worm or snail in their time of need - you will remember how sugar smells when it is poured into a bowl - you will know how it feels for a robin to startle you awake - you will think about what it would have been like to grow up by the sea - you will never be normal again - you will change - you will change - you will change - YOU WILL CHANGE - you will learn to tell the hours by looking at the light of the world - you will know this is a map and not the territory - you will redistribute this license along with the code --]] -- it is currently hardcoded to be in seattle. (its bring your own coordinates -- go look 'em up urself) also if you use this above the arctic circle it will -- probably break. if you have ideas for timekeeping in the arctic, reach out to -- me by searching tirelessly for someone who keeps changing their name, their -- face, their body, their language, and you lose hope you will ever know who i -- am, until one day, you see me on a train and you somehow just Know that it is -- Me, and you work up the courage to approach me but just as you clear your -- throat, I get off the train, and you stand asweved in the twilight. or find -- me on horny trans discord servers. local lat = 47.6 local lng = -122.3 -- set this to false in the summer local winter_mode=true -- set this to true if you want a table of hours!! local print_timetable_mode=false -- change these to whatever you want. add as many seasons as you want. add -- impossible seasons. add more hours, subtract more hours. add local seasons. -- add dreaming-seasons. rearrange the hour names. you are alive in a world -- where men no longer believe in unicorns and time is running out and all we -- can do is love each other and make magic. -- foxclock will adjust fine if you make it have more or fewer hours. local day_hour_names local night_hour_names if winter_mode then -- winter day_hour_names = { "halo", "steam", "pang", "mill", "glean", "gleam", "glance" } night_hour_names = { "gloam", "glow", "glim", "nill", "dream", "seem", "frost" } else -- summer day_hour_names = { "dew", "stir", "snack", "mill", "sprawl", "brill", "honey" } night_hour_names = { "owl's light", "still", "snuck", "nill", "dream", "seem", "robin's light" } end -- change the prefix for the part of the hour -- make them unfur. make them blossom. make them shake. local Early_Name = "early" local Mid_Name = "mid" local Late_Name = "late" -- 🐇 🐇 🐇 🐇 🐇 🐇 🐇 🐇 -- you now no longer have to read the code unless you're using this as -- a library then also have a look at the last few lines. -- now begins Most Of Suncalc.Lua which we just copypasted into here -- -- Thanks suncalc.lua! -- --[[ Copyright (c) 2014, Vladimir Agafonkin All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --]] -- for simple translation of javascript ternary comparison operator ?: local function ternary(a, b, c) if a then return b end return c end -- shortcuts for easier to read formulas local PI = math.pi local sin = math.sin local cos = math.cos local tan = math.tan local asin = math.asin local atan = math.atan local acos = math.acos local rad = PI / 180 -- sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas -- date/time constants and conversions local dayS = 60 * 60 * 24 local J1970 = 2440588 local J2000 = 2451545 local function toJulian(date) return date / dayS - 0.5 + J1970 end local function fromJulian(j) return (j + 0.5 - J1970) * dayS end local function toDays(date) return toJulian(date) - J2000 end -- general calculations for position local e = rad * 23.4397 -- obliquity of the Earth local function rightAscension(l, b) return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)) end local function declination(l, b) return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)) end local function azimuth(H, phi, dec) return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)) end local function altitude(H, phi, dec) return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)) end local function siderealTime(d, lw) return rad * (280.16 + 360.9856235 * d) - lw end local function astroRefraction(h) if (h < 0) then -- the following formula works for positive altitudes only. h = 0 end -- if h = -0.08901179 a div/0 would occur. -- formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. -- 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad: return 0.0002967 / math.tan(h + 0.00312536 / (h + 0.08901179)) end -- general sun calculations local function solarMeanAnomaly(d) return rad * (357.5291 + 0.98560028 * d) end local function eclipticLongitude(M) local C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)) -- equation of center local P = rad * 102.9372 -- perihelion of the Earth return M + C + P + PI end local function sunCoords(d) local M = solarMeanAnomaly(d) local L = eclipticLongitude(M) return { dec = declination(L, 0), ra = rightAscension(L, 0) } end local SunCalc = {} -- calculates sun position for a given date and latitude/longitude SunCalc.getPosition = function (date, lat, lng) local lw = rad * -lng local phi = rad * lat local d = toDays(date) local c = sunCoords(d) local H = siderealTime(d, lw) - c.ra return { azimuth = azimuth(H, phi, c.dec), altitude = altitude(H, phi, c.dec) } end -- sun times configuration (angle, morning name, evening name) SunCalc.times = { {-0.833, 'sunrise', 'sunset' }, { -0.3, 'sunriseEnd', 'sunsetStart' }, { -6, 'dawn', 'dusk' }, { -12, 'nauticalDawn', 'nauticalDusk'}, { -18, 'nightEnd', 'night' }, { 6, 'goldenHourEnd', 'goldenHour' } } -- adds a custom time to the times config SunCalc.addTime = function (angle, riseName, setName) table.insert(SunCalc.times, {angle, riseName, setName}) end -- calculations for sun times local J0 = 0.0009 local function julianCycle(d, lw) return math.floor(0.5 + (d - J0 - lw / (2 * PI))) end local function approxTransit(Ht, lw, n) return J0 + (Ht + lw) / (2 * PI) + n end local function solarTransitJ(ds, M, L) return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L) end local function hourAngle(h, phi, d) return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))) end local function observerAngle(height) return -2.076 * math.sqrt(height) / 60 end -- returns set time for the given sun altitude local function getSetJ(h, lw, phi, dec, n, M, L) local w = hourAngle(h, phi, dec) local a = approxTransit(w, lw, n) return solarTransitJ(a, M, L) end -- calculates sun times for a given date, latitude/longitude, and, optionally, -- the observer height (in meters) relative to the horizon SunCalc.getTimes = function (date, lat, lng, height, times) times = times or SunCalc.times height = height or 0 local lw = rad * -lng local phi = rad * lat local dh = observerAngle(height) local d = toDays(date) local n = julianCycle(d, lw) local ds = approxTransit(0, lw, n) local M = solarMeanAnomaly(ds) local L = eclipticLongitude(M) local dec = declination(L, 0) local Jnoon = solarTransitJ(ds, M, L) local h0, Jset, Jrise local result = { solarNoon = fromJulian(Jnoon), nadir = fromJulian(Jnoon - 0.5) } for _, time in ipairs(times) do h0 = (time[01] + dh) * rad Jset = getSetJ(h0, lw, phi, dec, n, M, L) Jrise = Jnoon - (Jset - Jnoon) result[time[2]] = fromJulian(Jrise) result[time[3]] = fromJulian(Jset) end return result end -- 🦊 🦊 🦊 🦊 🦊 -- okay suncalc over now we are in the foxclock zone -- -- this is a utility function for printing out tables local function tprint (tbl, indent) if not indent then indent = 0 end local toprint = string.rep(" ", indent) .. "{\r\n" indent = indent + 2 for k, v in pairs(tbl) do toprint = toprint .. string.rep(" ", indent) if (type(k) == "number") then toprint = toprint .. "[" .. k .. "] = " elseif (type(k) == "string") then toprint = toprint .. k .. "= " end if (type(v) == "number") then toprint = toprint .. v .. ",\r\n" elseif (type(v) == "string") then toprint = toprint .. "\"" .. v .. "\",\r\n" elseif (type(v) == "table") then toprint = toprint .. tprint(v, indent + 2) .. ",\r\n" else toprint = toprint .. "\"" .. tostring(v) .. "\",\r\n" end end toprint = toprint .. string.rep(" ", indent-2) .. "}" print(toprint) end local function hoursLater(date, h) -- dayS is a constant return date + h * dayS / 24 end --[[ So let's say that time_list is a **list** of times, in time-order. Like this: { { -10, "sunset yesterday" }, { -5, "sunrise today" }, { 5, "sunset today" }, { 10, "sunrise tomorrow" } } And then say `now` is 0. We want to know the two times we are inbetween. So in this case we would return "sunrise today", "sunset today" to show we are in the day-part of today. That's what this function does. You need to make sure that there are enough times here that we are in between two of them. If not... well that'd be weird. ]] local function when_even_are_we(now, time_list) for i in 1, #time_list - 1 do -- event time and name local etime_prev, ename_prev = table.unpack(time_list[i]) local etime_next, ename_next = table.unpack(time_list[i + 1]) if now >= etime_prev and now < etime_next then -- We are inbetween these two times return ename_prev, ename_next end end end --[[ This little friend will convert all our fun friendly hour names and say when they happen in the silly little world of "24 hour" that the "humans" like to use. ]] local function print_hour_timetable(fuzziest_time, suntimes) local day_length = suntimes.sunset - suntimes.sunrise local night_length = (60 * 60 * 24) + suntimes.sunrise - suntimes.sunset -- We want to print the night first if its night, and the day first if its -- day. then we print the other. local first, second -- We start off with these in day, night order. But then if it's night we -- swap them. The start time just needs to be the start of *a* segment, it -- doesn't really need to be exactly today's segment because itll be close -- enough. first = { start = suntimes.sunrise, length = day_length, names = day_hour_names } second = { start = suntimes.sunset, length = night_length, names = night_hour_names } if fuzziest_time == 'night' then -- swap for night local tmp = first first = second second = tmp end -- Do the first, then the second. for _, seg in ipairs({first, second}) do -- How long is a hour? local hourlen = seg.length / #seg.names -- Now go through the segment, one hourlength at a time. Print out what -- 24-hour time it would be, and our hour name. for i = 1, #seg.names do local t = math.floor(seg.start + (i - 1) * hourlen) -- 24hour date, without the day -- For some reason lua5.4 on debian doesn't support %l in strftime, -- so we use %I which formats it with a leading 0, and then replace -- that with a space. lol local time_of_day_normalpeople = os.date('%I:%M %p', t):gsub('^0', ' ') local hour_name = seg.names[i] print(time_of_day_normalpeople .. ' - ' .. hour_name) end end end local function print_hour_timetable_for_date(date) -- HEY BUDDY YOU CANT BE COPY PASTING LAT/LNG LIKE THAT BUDDY -- probably somethings in the code should rearrange... local time = os.time(date) --[[ local lat = 47.6 local lng = -122.3 --]] local suntimes = SunCalc.getTimes(time, lat, lng) print_hour_timetable("day", suntimes) end --[[ For anything that wants to use us as a library, here's how they can do it! Any function we put in "module" is a thing they can use ]] local foxclock = {} function foxclock.what_time_is_it(now) -- tprint(SunCalc.getPosition(now, lat, lng)) -- tprint(SunCalc.getTimes(now, lat, lng)) --[[ This gives us the times of a bunch of interesting solar events, with their absolute timestamp in seconds since the epoch. Some of them will be before Now, and some of them will be after Now. Specifically, we have: - nadir - nightEnd - nauticalDawn - dawn - sunrise - sunriseEnd - goldenHour - goldenHourEnd - solarNoon - sunsetStart - sunset - dusk - nauticalDusk - night ]] local suntimes = SunCalc.getTimes(now, lat, lng) --[[ We want to know if it is day time or not time. If we are between sunrise and sunset for the day, then that means it's day time! Otherwise it is night. It is up to you, really, whether to use the start or end of the sunrise and sunset. but for now we will use `sunrise` and `sunset` ]] local fuzziest_time local seg_start local seg_end if now > suntimes.sunrise and now < suntimes.sunset then -- DAY fuzziest_time = 'day' seg_start = suntimes.sunrise seg_end = suntimes.sunset else -- NIGHT fuzziest_time = 'night' --[[print(now) print(suntimes.sunrise) print(suntimes.sunset) --]] --[[ If we are on the left of sunrise, then the segment ends at sunrise and starts at the *previous days* sunset. If we are on the right of sunset, then sunset is when the segment started and it will end on tomorrows sunrise. --]] if now < suntimes.sunrise then --[[ For now, do the inaccurate but maybe close enough thing of taking 24 hours off of today's number. This will be least accurate around the equinox and most accurate around the solstice. --]] seg_start = suntimes.sunset - 60 * 60 * 24 seg_end = suntimes.sunrise else seg_start = suntimes.sunset seg_end = suntimes.sunrise + 60 * 60 * 24 --print(seg_start) --print(seg_end) end end --[[ Now we want to know how far through the current time-section we are. We want this as like a fraction out of 1.0, because we're going to re-scale it to our own fucked up time system :3 :3 :3 ]] local seg_progression = (now - seg_start) / (seg_end - seg_start) --[[ now we re-scale it to ""hours"", which are some number of divisions of the segment. We will use the length of the names list to decide this, that way we don't need to remember to update both places when we make changes. Plus then you can have the day and night have different number of hours, and isnt that kinda neat? ]] local num_hours if fuzziest_time == 'day' then num_hours = #day_hour_names else num_hours = #night_hour_names end local current_hour = math.floor(seg_progression * num_hours) --[[print("current hour") print(current_hour)--]] --[[ If seg_progression is **exactly** 1.0, then current_hour can be num_hours. Which is weird. For a very small instant you might have that, if the maths work out that way. But that adds a secret num_hours + 1 hour which doesnt fit. So if current_hour == num_hours we should make it be num_hours - 1 for that instant. ]] if current_hour == num_hours then current_hour = num_hours - 1 end --[[ But what is this hour named?? Let's find out! Don't forget to add 1, because current_hour from the maths we did will be from 0 to (num_hours - 1) ]] local hour_name if fuzziest_time == 'day' then hour_name = day_hour_names[current_hour + 1] else hour_name = night_hour_names[current_hour + 1] end --[[ let's do math to find out how far we are within the hour ]] local hour_length = (seg_end - seg_start) / num_hours --[[]] local current_hour_start = seg_start + ((current_hour)*hour_length) local current_hour_progress = (now - current_hour_start) / hour_length --print(string.format("now %s current_hour %s seg_start %s seg_end %s hour_length %s current_hour_start %s current_hour_progress", now, current_hour, seg_start, seg_end, hour_length, current_hour_start, current_hour_progress)) local minute_name if current_hour_progress < 0 then minute_name = "errneg" elseif current_hour_progress <= 0.33 then minute_name = Early_Name elseif current_hour_progress <= 0.66 then minute_name = Mid_Name elseif current_hour_progress <= 1 then minute_name = Late_Name else minute_name = "errpos" end --[[ And now we know what time it is! ]] if print_timetable_mode then print_hour_timetable("day", suntimes) end return string.format("%s %s", minute_name, hour_name) end local function mane() local now = os.time() print(foxclock.what_time_is_it(now)) end -- swap these comments out to use this as a library (for instance, to put this on koreader) -- return foxclock mane()