#!/usr/bin/perl -w use strict; use Time::Local; # Number of horizontal calendars to draw. use constant CALENDAR_COUNT => 2; # Color bitmasks. use constant CURRENT_MONTH => 1; use constant CURRENT_DAY => 2; use constant FULL_MOON => 4; # All variables. my ($sec, $min, $hour, $mday, $month, $year, $wday, @current, @t, @colors, $i, $j, $k, $l); # Color palette, indexed by bitmask. my @palette = ( "\e[90;40m", "\e[37;40m", # CURRENT_MONTH "\e[30;47m", # CURRENT_DAY "\e[30;47m", # CURRENT_MONTH | CURRENT_DAY "\e[90;40m", # FULL_MOON (not current month) "\e[96;40m", # FULL_MOON | CURRENT_MONTH "\e[30;106m", # FULL_MOON | CURRENT_DAY # CURRENT_MONTH | CURRENT_DAY | FULL_MOON is handled specially. ); # Align timestamp to midnight. sub Align($$) { my ($t, $align_mday) = @_; ($sec, $min, $hour, $mday, $month, $year) = localtime($t); return timelocal(0, 0, 0, $align_mday ? 1 : $mday, $month, $year); } # Draw space between rows. sub OddRow($$) { my ($index, $colors) = @_; for($k = 0; $k < 7; $k++, $index++) { my $below = $$colors[$index]; my $above = $index >= 7 ? $$colors[$index - 7] : 0; my $bits = $above | $below; # Special handling for when row above or below is the current date, # and the other row is a full moon. We can't simply use a bitwise # OR for this because we need to distinguish the two cases: # (CURRENT_MONTH | FULL_MOON) | CURRENT_DAY -> CURRENT_DAY # (CURRENT_DAY | FULL_MOON) | CURRENT_MONTH -> CURRENT_MONTH | FULL_MOON $bits = $bits - (CURRENT_MONTH | CURRENT_DAY | FULL_MOON) ? $bits : CURRENT_DAY | ($above - CURRENT_DAY && $below - CURRENT_DAY ? FULL_MOON : 0); print "$palette[$bits] \e[0m "; } } # Draw date numbers. sub EvenRow($$$) { my ($t, $index, $colors) = @_; for($k = 0; $k < 7; $k++, $index++) { ($sec, $min, $hour, $mday) = localtime($t); printf '%s %2d '."\e[0m ", $palette[$$colors[$index]], $mday; $t = Align($t + 86400 + 43200, 0); } return $t; } # Get dates for current month. @t = (time); if( $#ARGV == 0 ) { if( $ARGV[0] !~ /^(\d+)-(\d+)-(\d+)$/ || !defined($t[0] = eval { timelocal(0, 0, 0, $3, $2 - 1, $1 - 1900) }) ) { die "Unable to parse $ARGV[0] as YYYY-MM-DD\n"; } } for($i = 0; $i < CALENDAR_COUNT; $i++) { ($sec, $min, $hour, $mday, $month, $year) = localtime($l = $t[$i]); push @current, [$year, $month, $i == 0 ? $mday : -1]; # Go back to first day of the month, then go back to first Sunday. for($l = Align($l, 1);; $l = Align($l - 43200, 0)) { ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($l); last if !$wday; } $t[$i] = $l; my @c = (); my @age = (); for($j = 0; $j < 6 * 7; $j++) { ($sec, $min, $hour, $mday, $month, $year, $wday, $k) = localtime($l); # Record moon age for each date. This is calculated based on # number of days since 1900-01-01, which happens to be a new moon. # An easier way to compute this is to just take the number of # unix seconds and do a modulus, but that relies on the unix # epoch being fixed, which isn't true for all platforms. $k += $year * 365 + int(($year - 1) / 4) - int(($year - 1) / 100) + int(($year - 1 + 1900) / 400) - 4; # Length of synodic month: # https://en.wikipedia.org/wiki/Lunar_phase#Calculating_phase use constant PERIOD => 29.530588853; $k -= int($k / PERIOD) * PERIOD; push @age, $k; # Set color based on whether displayed date matches current date. push @c, ($month - $current[$i][1] ? 0 : $mday - $current[$i][2] ? CURRENT_MONTH : CURRENT_DAY); $l = Align($l + 86400 + 43200, 0); } # Now update colors based on whether moon age between midnights contains # 15.5, and also add padding for bottommost row. for($j = 0; $j < 6 * 7 - 1; $j++) { if( $age[$j] <= 15.5 && $age[$j + 1] > 15.5 ) { $c[$j] |= FULL_MOON; } push @c, 0; } push @colors, [@c]; # Draw headings. print " " if $i; printf "\e[97;40m\e[4m %04d-%02d" . (" " x 26) . "\e[0m", $current[$i][0] + 1900, $current[$i][1] + 1; # Step forward 6 weeks from first Sunday of current month. This is # guaranteed to put us somewhere in the next month, and definitely not # the month after that. push @t, $t[$i] + 86400 * 6 * 7; } print "\n"; # Draw calendars. for($j = 0; $j < 7; $j++) { for($i = 0; $i < CALENDAR_COUNT; $i++) { print " " if $i; OddRow($j * 7, $colors[$i]); } print "\n"; if( $j < 6 ) { for($i = 0; $i < CALENDAR_COUNT; $i++) { print " " if $i; $t[$i] = EvenRow($t[$i], $j * 7, $colors[$i]); } print "\n"; } }