#!/usr/bin/perl -w use strict; use Time::Local; # Color bitmasks. use constant CURRENT_MONTH => 1; use constant CURRENT_DAY => CURRENT_MONTH | 2; use constant FULL_MOON => 4; # Color palette, indexed by bitmask. my @palette = ( "\e[90;40m", "\e[37;40m", # CURRENT_MONTH undef, "\e[30;47m", # CURRENT_DAY "\e[90;40m", "\e[96;40m", # CURRENT_MONTH | FULL_MOON "\e[90;40m", # FULL_MOON "\e[30;106m", # CURRENT_DAY | FULL_MOON ); # Get midnight of previous day. We can't simply subtract 24 hours since # number of hours in each day might be different due to daylight savings. sub PreviousDay($) { my ($t) = @_; # Assuming that input timestamp is already aligned to midnight, # going back 12 hours is guaranteed to put us in the previous day. my ($sec, $min, $hour, $mday, $mon, $year) = localtime($t - 43200); return timelocal(0, 0, 0, $mday, $mon, $year); } # Get midnight of next day. sub NextDay($) { my ($t) = @_; my ($sec, $min, $hour, $mday, $mon, $year) = localtime($t + 86400 + 43200); return timelocal(0, 0, 0, $mday, $mon, $year); } # Get first Sunday on or before first day of month. sub GetMonthStart($) { my ($t) = @_; # Go back to first day of the month. my ($sec, $min, $hour, $mday, $mon, $year, $wday) = localtime($t); $t = timelocal(0, 0, 0, 1, $mon, $year); # Go back to first Sunday. for(;;) { ($sec, $min, $hour, $mday, $mon, $year, $wday) = localtime($t); last if $wday == 0; $t = PreviousDay($t); } return $t; } # Decide how to color each day of the calendar. sub GetColors($$$) { my ($t, $current_month, $current_day) = @_; my @colors = (); my @age = (); for(my $day = 0; $day < 6 * 7; $day++) { my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = localtime($t); # 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. my $days = $year * 365 + int(($year - 1) / 4) - int(($year - 1) / 100) + int(($year - 1 + 1900) / 400) - 4 + $yday; # Length of synodic month: # https://en.wikipedia.org/wiki/Lunar_phase#Calculating_phase use constant PERIOD => 29.530588853; $days -= int($days / PERIOD) * PERIOD; push @age, $days; # Set color based on whether displayed date matches current date. my $bits = 0; if( $mon == $current_month ) { $bits = CURRENT_MONTH; if( $mday == $current_day ) { $bits = CURRENT_DAY; } } push @colors, $bits; $t = NextDay($t); } for(my $pad = 0; $pad < 7; $pad++) { push @colors, 0; push @age, 0; } # Now update colors based on whether moon age between midnights contains # 15.5. for(my $day = 0; $day < 6 * 7 - 1; $day++) { if( $age[$day] <= 15.5 && $age[$day + 1] > 15.5 ) { $colors[$day] |= FULL_MOON; } } (scalar @colors) == 7 * 7 or die; return @colors; } # Draw month heading. sub Heading($$) { my ($year, $month) = @_; printf "\e[97;40m\e[4m %04d-%02d" . (" " x 26) . "\e[0m", $year, $month; } # Draw space between rows. sub OddRow($$) { my ($index, $colors) = @_; for(my $day = 0; $day < 7; $day++, $index++) { my $bits = $$colors[$index] | $$colors[($index + 6 * 7) % (7 * 7)]; print $palette[$bits], " " x 4; if( $day < 6 ) { print "\e[37;40m "; } else { print "\e[0m"; } } } # Draw date numbers. sub EvenRow($$$) { my ($t, $index, $colors) = @_; for(my $day = 0; $day < 7; $day++, $index++) { my $bits = $$colors[$index]; my ($sec, $min, $hour, $mday) = localtime($t); printf '%s %2d ', $palette[$bits], $mday; if( $day < 6 ) { print "\e[37;40m "; } else { print "\e[0m"; } $t = NextDay($t); } return $t; } # Get dates for current month. my $now = time; $now = 1611273600; my ($sec, $min, $hour, $current_day, $current_month, $current_year) = localtime($now); my $t0 = GetMonthStart($now); # 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. my $t1 = $t0 + 86400 * 6 * 7; my ($next_day, $next_month, $next_year); ($sec, $min, $hour, $next_day, $next_month, $next_year) = localtime($t1); $t1 = GetMonthStart($t1); $next_day = -1; my @colors0 = GetColors($t0, $current_month, $current_day); my @colors1 = GetColors($t1, $next_month, $next_day); # Draw calendars. Heading($current_year + 1900, $current_month + 1); print " "; Heading($next_year + 1900, $next_month + 1); print "\n"; for(my $week = 0; $week < 6; $week++) { OddRow($week * 7, \@colors0); print " "; OddRow($week * 7, \@colors1); print "\n"; $t0 = EvenRow($t0, $week * 7, \@colors0); print " "; $t1 = EvenRow($t1, $week * 7, \@colors1); print "\n"; } OddRow(6 * 7, \@colors0); print " "; OddRow(6 * 7, \@colors1); print "\n";