The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl

use strict;
use warnings;
use autodie;

use lib './lib';

my $VERSION = 0.03;

my $leap = shift || './leaptab.txt';

my $x = 1;
my %months = map { $_ => $x++ }
    qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );

my @LeapSeconds;
my @RD;
my %RDLength;

build_data_structures();
write_header();

sub build_data_structures {
    open my $fh, '<', $leap;

    my $value = -1;
    while (<$fh>) {
        my ( $year, $mon, $day, $leap_seconds ) = split /\s+/;

        $mon =~ s/\W//;

        $leap_seconds =~ s/^([+-])//;
        my $mult = $1 eq '+' ? 1 : -1;

        my $utc_epoch = _ymd2rd( $year, $months{$mon}, $day );

        $value += $leap_seconds * $mult;

        push @LeapSeconds, $value;
        push @RD,          $utc_epoch;

        $RDLength{ $utc_epoch - 1 } = $leap_seconds;
    }

    close $fh;

    push @LeapSeconds, ++$value;
}

sub write_header {
    my $set_leap_seconds = <<"EOF";

#define SET_LEAP_SECONDS(utc_rd, ls)  \\
{                                     \\
  {                                   \\
    if (utc_rd < $RD[0]) {            \\
      ls = $LeapSeconds[0];           \\
EOF

    for ( my $x = 1; $x < @RD; $x++ ) {
        my $else = $x == 1 ? '' : 'else ';

        my $condition
            = $x == @RD
            ? "utc_rd < $RD[$x]"
            : "utc_rd >= $RD[$x - 1] && utc_rd < $RD[$x]";

        $set_leap_seconds .= <<"EOF"
    } else if ($condition) {  \\
      ls = $LeapSeconds[$x];                      \\
EOF
    }

    $set_leap_seconds .= <<"EOF";
    } else {                         \\
      ls = $LeapSeconds[-1];       \\
    }                              \\
  }                                \\
}
EOF

    my $set_extra_seconds = <<"EOF";

#define SET_EXTRA_SECONDS(utc_rd, es)  \\
{                                      \\
  {                                    \\
    es = 0;                            \\
    switch (utc_rd) {                  \\
EOF

    my $set_day_length = <<"EOF";

#define SET_DAY_LENGTH(utc_rd, dl)     \\
{                                      \\
  {                                    \\
    dl = 86400;                        \\
    switch (utc_rd) {                  \\
EOF

    foreach my $utc_rd ( sort keys %RDLength ) {
        $set_extra_seconds .= <<"EOF";
      case $utc_rd: es = $RDLength{$utc_rd}; break;            \\
EOF

        $set_day_length .= <<"EOF";
      case $utc_rd: dl = 86400 + $RDLength{$utc_rd}; break;    \\
EOF
    }

    $set_extra_seconds .= <<"EOF";
    }                                  \\
  }                                    \\
}
EOF

    $set_day_length .= <<"EOF";
    }                                  \\
  }                                    \\
}
EOF

    my $header = <<"EOF";
/* This file is auto-generated by the leap second code generator
  ($VERSION).  This code generator comes with the DateTime.pm module
  distribution in the tools/ directory

  Generated by $0.

  Do not edit this file directly.
*/
EOF

    open my $fh, '>', 'c/leap_seconds.h';

    print $fh (
        $header,
        $set_leap_seconds,
        $set_extra_seconds,
        $set_day_length,
    );
}

# from lib/DateTimePP.pm
sub _ymd2rd {
    use integer;
    my ( $y, $m, $d ) = @_;
    my $adj;

    # make month in range 3..14 (treat Jan & Feb as months 13..14 of
    # prev year)
    if ( $m <= 2 ) {
        $y -= ( $adj = ( 14 - $m ) / 12 );
        $m += 12 * $adj;
    }
    elsif ( $m > 14 ) {
        $y += ( $adj = ( $m - 3 ) / 12 );
        $m -= 12 * $adj;
    }

    # make year positive (oh, for a use integer 'sane_div'!)
    if ( $y < 0 ) {
        $d -= 146097 * ( $adj = ( 399 - $y ) / 400 );
        $y += 400 * $adj;
    }

    # add: day of month, days of previous 0-11 month period that began
    # w/March, days of previous 0-399 year period that began w/March
    # of a 400-multiple year), days of any 400-year periods before
    # that, and 306 days to adjust from Mar 1, year 0-relative to Jan
    # 1, year 1-relative (whew)

    $d
        += ( $m * 367 - 1094 ) / 12
        + $y % 100 * 1461 / 4
        + ( $y / 100 * 36524 + $y / 400 )
        - 306;
}