My custom DeadJournal and LiveJournal themes have always included the phase of the Moon for every post, and I've been jotting down dates in the same way since abount 2001. So I've had a homebrew Moon phase calculator for LiveJournal (and friends) sites for a while. This is tricky, because the LiveJournal templating language, S2, is purposefully limited in what it can do. So this qualifies as a hack, and ‘calculator’ is a little inaccurate.
Source Code
function pom (DateTime t): int
{
var int cof;
var int i;
var int pom;
var int best;
var int[] daymon = [0,0,31,59,90,120,151,181,212,243,273,304,334];
var int[] fullmoons = [
10150, 10179, 10209, 10239, 10268, 10298, 10328, 10357, 10387,
10417, 10446, 10475, 10505, 10534, 10564, 10593, 10623, 10652,
10682, 10711, 10741, 10771, 10800, 10830, 10859, 10889, 10918,
10948, 10977, 11007, 11036, 11066, 11095, 11125, 11154, 11184,
11214, 11243, 11273, 11302, 11332, 11361, 11391, 11420, 11449,
11479, 11508, 11538, 11568, 11597, 11627, 11657, 11686, 11716,
11745, 11775, 11804, 11833, 11863, 11892, 11922, 11951, 11981,
12011, 12041, 12070, 12100, 12129, 12159, 12188, 12217, 12247,
12276, 12306, 12335, 12365, 12395, 12424, 12454, 12484, 12513,
12543, 12572, 12601, 12631, 12660, 12689, 12719, 12749, 12778,
12808, 12838, 12868, 12897, 12927, 12956, 12985, 13015, 13044,
13073, 13103, 13132, 13162, 13192, 13222, 13251, 13281, 13311,
13340, 13369, 13399, 13428, 13457, 13487, 13516, 13546, 13576,
13605, 13635, 13665, 13694, 13724, 13753, 13783, 13812, 13842,
13871, 13900, 13930, 13960, 13989, 14019, 14049, 14078, 14108,
14137, 14167, 14196, 14226, 14255, 14285, 14314, 14343, 14373,
14403, 14432, 14462, 14491, 14521, 14551, 14580, 14610, 14639,
14669, 14698, 14727, 14757, 14786, 14816, 14845, 14875, 14905,
14935, 14964, 14994, 15023, 15053, 15082, 15111, 15141, 15170,
15200, 15229, 15259, 15289, 15318, 15348, 15378, 15407, 15437,
15466, 15495, 15525, 15554, 15583, 15613, 15643, 15672, 15702,
15732, 15762, 15791, 15821, 15850, 15879, 15909, 15938, 15967,
15997, 16026, 16056, 16086, 16116, 16146, 16175, 16205, 16234,
16263, 16293, 16322, 16351, 16381, 16410, 16440, 16470, 16500,
16529, 16559, 16589, 16618, 16647, 16677, 16706, 16735, 16765,
16794, 16824, 16854, 16883, 16913, 16943, 16972, 17002, 17031,
17061, 17090, 17120, 17149, 17178, 17208, 17237, 17267, 17297,
17326, 17356, 17386, 17415, 17445, 17474, 17504, 17533, 17562,
17592, 17621, 17651, 17680, 17710, 17740, 17769, 17799, 17829,
17858, 17888, 17917, 17947, 17976, 18005, 18035, 18064, 18094,
18123, 18153, 18183, 18212, 18242, 18272, 18301, 18331, 18360,
18389, 18419, 18448, 18477, 18507, 18537, 18566, 18596, 18626,
18656, 18685, 18715, 18744, 18773, 18803, 18832, 18861, 18891,
18920, 18950, 18980, 19010, 19040, 19069, 19099, 19128, 19157,
19187, 19216, 19245, 19275, 19304, 19334, 19364, 19394, 19423,
19453, 19483, 19512, 19541, 19571, 19600, 19629, 19659, 19688,
19718, 19748, 19777, 19807, 19837, 19866, 19896, 19925, 19955,
19984, 20013, 20043, 20072, 20102, 20131, 20161, 20191, 20220,
20250, 20280, 20309, 20339, 20368, 20397, 20427, 20456, 20486,
20515, 20545, 20574, 20604, 20634, 20663, 20693, 20723, 20752,
20782, 20811, 20840, 20870, 20899, 20929, 20958, 20988, 21017,
21047, 21077, 21106, 21136, 21166, 21195, 21225, 21254, 21283,
21313, 21342, 21372, 21401, 21431, 21460, 21490, 21520, 21550,
21579, 21609, 21638, 21667, 21697, 21726, 21755, 21785, 21814,
21844, 21874, 21904, 21933, 21963, 21993, 22022, 22051, 22081,
22110, 22139, 22169, 22198, 22228, 22258, 22288, 22317, 22347,
22377, 22406, 22435, 22465, 22494, 22523, 22553, 22582, 22612,
22641, 22671, 22701, 22731, 22760, 22790, 22819, 22849, 22878,
22907, 22937, 22966, 22996, 23025, 23055, 23085, 23115, 23144,
23174, 23203, 23233
];
var bool isleap = false;
var int y;
# Is this a leap year?
if ((($t.year % 4) == 0) and
((($t.year % 100) !=0) or
(($t.year % 400) == 0))) {
$isleap = true;
}
$y = $t.year - 1970;
$cof = 365 * $y;
# Correct by .25 for each leap year (add 1 as an adjustment
# because 1972 is the first leap year after 1970). Sanity check:
# (base 1970) 0=>0, 1=>0, 2=>0, 3=>1, 4=>1, 5=>1, 6=>1, 7=>2, 8=>2, etc.
$cof = $cof + ($y + 1) / 4;
# Correct for centuries. This should be easy, really.
$cof = $cof - ($y + 69) / 100;
# Correct for 400-year leap exceptions.
$cof = $cof + ($y + 369) / 400;
# Summary of leap year rule: a year is leap if divisible by 4,
# and NOT divisible by 100, UNLESS also divisible by 400.
# Now add the length of the months that have passed.
$cof = $cof + $daymon[$t.month];
if ($isleap and ($t.month > 2)) {
$cof = $cof + 1;
}
$cof = $cof + $t.day - 1;
# This is bad programming, but S2 only has fundamental flow control.
foreach $i ($fullmoons) {
if ($i <= $cof) {
$best = $i;
}
}
# How many days since the last full Moon? Express it in minutes
# for greater accuracy. The Moon's period is 42,532 minutes.
$cof = ($cof - $best) * 1440;
# We return a value between -14 and 14, such as:
#
# The absolute value is the fullness of the Moon.
# The sign signifies waxing (positive) or waning (negative) Moon.
$pom = ($cof * 28 / 42532) - 14;
return $pom;
}
Explanation
S2 is a simple Turing-complete templating language heavily based on Perl (and interpreted by same). It's okay for simple layouts, but lacks good flow control and mathematics.
The latter flaw means having to use a table of precalculated full Moons (and calculating stupidly at times). There's one table entry for every month of every year starting with, if I remember correctly, 1997—To this code will be able to calculate the phase of the Moon for all non-backdated LiveJournal posts, which was founded a couple of years later. If you need more entries, feel free to add them. Each entry in the table represents the Unix time: seconds since the first of January, 1970, UTC, excluding leap seconds.
It's been a long time, but I believe the table of full Moons and some of the code was adapted from the source of the BSD ‘game’ program pom(6)
. If you have this on your system, you may write a script to get more full-Moon days.
Other than that, function pom
accepts a DateTime
object, and calculates the day number it represents. It then figures out the last full Moon before that date, and figures out the phase of the Moon at that date based on the Moon's period (42,532 minutes).
It returns an integer between -14
and +14
, which allows for 29 distinct values (the Moon's synodic period, give or take). A value of zero represents the new Moon. Negative values represent a waning Moon, positive values a waxing Moon. Read on for a couple of examples of using this to render a Moon phase icon and a textual description.
Using It
If you read the above, using the function should be quite easy. The following two functions accept a DateTime
object each, and respectively return a textual representation of the phase of the Moon at that date and time (very similar to what the BSD program pom(6)
returns), and the URI of an icon representing the Moon phase.
Textual Representation
Here's how to translate the output of pom()
to text. It should be very straightforward.
function pomText (DateTime t): string
{
var int n;
$n = pom ($t);
if ($n <= -13) {
return "full";
} elseif ($n < -7) {
return "waning gibbous";
} elseif ($n == -7) {
return "in its last quarter";
} elseif ($n < 0) {
return "waning crescent";
} elseif ($n == 0) {
return "new";
} elseif ($n < 7) {
return "waxing crescent";
} elseif ($n == 7) {
return "in its first quarter";
} elseif ($n < 13) {
return "waxing gibbous";
}
return "full";
}
Generating Image URIs
Here's how to generate an image URI for each phase of the Moon:
function pomIcon (DateTime t): string
{
var int n;
$n = pom ($t);
return "https://example.com/moon/pom-" + $n + ".gif";
}
You will need to make available 29 different icons for all possible pom()
return values of -14
to 14
. The values aren't zero-padded. In this case, an example waning gibbous Moon could return a URI of http://example.com/moon/pom--10.gif
, while a Moon in its first quarter would request http://example.com/moon/pom-7.gif
. Obviously, changing the last line of the function will change what URIs are generated and how.
You could also use a modified pomText()
function to render fewer, or more descriptive URIs, if you want that.
And since this was written in the days of CSS2 and before image sprites, yes: you could use a single image as a sprite with appropriate an style="background-position: ..."
attribute. Although a more valid question would be: what are you doing using S2 in this day and age?