=head1 NAME

Time::TAI::Now - determine current time in TAI

=head1 SYNOPSIS

    use Time::TAI::Now qw(
	now_tai_rat now_tai_gsna now_tai_flt now_tai_dec);

    ($instant, $bound) = now_tai_rat;
    ($instant, $bound) = now_tai_rat(1);
    ($instant, $bound) = now_tai_gsna;
    ($instant, $bound) = now_tai_gsna(1);
    ($instant, $bound) = now_tai_flt;
    ($instant, $bound) = now_tai_flt(1);
    ($instant, $bound) = now_tai_dec;
    ($instant, $bound) = now_tai_dec(1);

=head1 DESCRIPTION

This module is one answer to the question "what time is it?".
It determines the current time on the TAI scale, and puts a bound on how
inaccurate it could be.  It is designed to interoperate with L<Time::TAI>,
which knows all about the TAI time scale.

TAI (International Atomic Time) is a time scale produced by an ensemble of
atomic clocks around Terra.  It attempts to tick at the rate of proper
time on the Terran geoid (i.e., at sea level).  It is the frequency
standard underlying Coordinated Universal Time (UTC).

TAI is not connected to planetary rotation, and so has no inherent
concept of a "day" or of "time of day".  (There is nevertheless a
convention for how to represent TAI times using day-based notations,
for which see L<Time::TAI>.)  This module represents instants on the
TAI time scale as a scalar number of TAI seconds since its epoch, which
was at 1958-01-01T00:00:00.0 UT2 as calculated by the United States
Naval Observatory.  This matches the convention used by C<Time::TAI>.

=cut

package Time::TAI::Now;

{ use 5.006; }
use warnings;
use strict;

use Data::Float 0.008 qw(significand_step float_parts mult_pow2);
use Math::BigRat 0.10;
use Math::Decimal 0.000 qw(dec_add);
use Time::UTC 0.007 qw(utc_to_tai);
use Time::UTC::Now 0.012 qw(now_utc_rat now_utc_sna now_utc_flt now_utc_dec);

our $VERSION = "0.004";

use parent "Exporter";
our @EXPORT_OK = qw(now_tai_rat now_tai_gsna now_tai_flt now_tai_dec);

use constant BIGRAT_ZERO => Math::BigRat->new(0);

=head1 FUNCTIONS

Each of these functions determines the current TAI time and returns it.
They vary in the form in which the time is returned.  In each case,
the function returns a list of two values.  The first value identifies
a current TAI instant, in the form of a number of seconds since the TAI
epoch.  The second value is an inaccuracy bound, as a number of seconds,
or C<undef> if no accurate answer could be determined.

If an inaccuracy bound is returned then the function is claiming to have
answered correctly, to within the specified margin.  That is, some instant
during the execution of the function is within the specified margin of
the instant identified.  (This semantic differs from older current-time
interfaces that are content to return an instant that has already passed.)
The inaccuracy bound describes the actual time represented in the first
return value, not some internal value that was rounded to generate the
return value.

The inaccuracy bound is measured in TAI seconds; that is, in SI seconds
on the Terran geoid as realised by atomic clocks.  This differs from SI
seconds at the computer's location, but the difference is only apparent
if the computer hardware is significantly time dilated with respect to
the geoid.

If C<undef> is returned instead of an inaccuracy bound then the function
could not find a trustable answer.  Either the clock available was not
properly synchronised or its accuracy could not be established.  Whatever
time could be found is returned, but the function makes no claim that it
is accurate.  It should be treated with suspicion.  In practice, clocks
of this nature are especially likely to misbehave around UTC leap seconds.

Each function will C<die> if it can't find a plausible time at all.
If the I<DEMAND_ACCURACY> parameter is supplied and true then it will
also die if it could not find an accurate answer, instead of returning
with C<undef> for the inaccuracy bound.

=over

=item now_tai_rat([DEMAND_ACCURACY])

Both return values are in the form of C<Math::BigRat> objects.

This retains full resolution, is future-proof, and is easy to manipulate,
but beware that C<Math::BigRat> is currently rather slow.  If performance
is a problem then consider using one of the functions below that return
the results in other formats.

=cut

my $rat_last_dayno = BIGRAT_ZERO;
my $rat_mn_s = BIGRAT_ZERO;

sub now_tai_rat(;$) {
	my($dayno, $secs, $bound) = now_utc_rat($_[0]);
	if($dayno != $rat_last_dayno) {
		$rat_mn_s = utc_to_tai($dayno, BIGRAT_ZERO);
		$rat_last_dayno = $dayno;
	}
	return ($rat_mn_s + $secs, $bound);
}

=item now_tai_gsna([DEMAND_ACCURACY])

The time since the epoch and the inaccuracy bound (if present) are each
returned in the form of a four-element array, giving a high-resolution
fixed-point number of seconds.  The first element is the integral number
of gigaseconds, the second is an integral number of seconds in the range
[0, 1000000000), the third is an integral number of nanoseconds in the
same range, and the fourth is an integral number of attoseconds in the
same range.

This form of return value is fairly efficient.  It is convenient for
decimal output, but awkward to do arithmetic with.  Its resolution is
adequate for the foreseeable future, but could in principle be obsoleted
some day.

The number of gigaseconds will exceed 1000000000, thus violating
the intent of the number format, one exasecond after the epoch,
when the universe is around three times the age it had at the epoch.
Terra (and thus TAI) might still exist then, depending on how much
its orbital radius increases before Sol enters its red giant phase.
In that situation the number of gigaseconds will simply continue to
increase, ultimately overflowing if native integer formats don't grow,
though it's a good bet that they will.

=cut

my $gsna_last_dayno = 0;
my($gsna_mn_g, $gsna_mn_s) = (0, 0);

sub now_tai_gsna(;$) {
	my($dayno, $secs, $bound) = now_utc_sna($_[0]);
	if($dayno != $gsna_last_dayno) {
		my $midnight = utc_to_tai(Math::BigRat->new($dayno),
					  BIGRAT_ZERO);
		$gsna_mn_g = ($midnight / 1000000000)->bfloor->numify;
		$gsna_mn_s = ($midnight % 1000000000)->numify;
		$gsna_last_dayno = $dayno;
	}
	my($g, $s) = ($gsna_mn_g, $gsna_mn_s);
	$s += $secs->[0];
	if($s >= 1000000000) {
		$g++;
		$s -= 1000000000;
	}
	$bound = [ 0, @$bound ] if defined $bound;
	return ([ $g, $s, @{$secs}[1, 2] ], $bound);
}

=item now_tai_flt([DEMAND_ACCURACY])

Both return values are in the form of Perl floating point numbers.

This form of return value is very efficient and easy to manipulate.
However, its resolution is limited, rendering it already obsolete for
high-precision applications at the time of writing.

=cut

my $flt_last_dayno = 0;
my $flt_mn_s = 0;
my $flt_add_bound = 0;

sub now_tai_flt(;$) {
	my($dayno, $secs, $bound) = now_utc_flt($_[0]);
	if($dayno != $flt_last_dayno) {
		$flt_mn_s = utc_to_tai(Math::BigRat->new($dayno), BIGRAT_ZERO)
				->numify;
		# Part of the precision of the number of seconds within
		# the day will be lost due to it being moved down the
		# significand to line up with the seconds derived from
		# the day number.  Not trusting floating-point rounding,
		# presume the maximum possible additional error to be 1
		# ulp of the final value.  That's 1 ulp of ($flt_mn_s +
		# 86400) at the end of the day; possibly 0.5 ulp of that
		# at the start of the day (if $flt_mn_s is just below an
		# exponent boundary), but using the larger value all day
		# will be fine.
		my(undef, $mn_exp, undef) = float_parts($flt_mn_s + 86400.0);
		$flt_add_bound = mult_pow2(significand_step, $mn_exp);
		$flt_last_dayno = $dayno;
	}
	$bound += $flt_add_bound if defined $bound;
	return ($flt_mn_s + $secs, $bound);
}

=item now_tai_dec([DEMAND_ACCURACY])

Each return value is in the form of a string expressing a number
as a decimal fraction.  These strings are of the type processed
by L<Math::Decimal>, and are always returned in L<Math::Decimal>'s
canonical form.

This form of return value is fairly efficient and easy to manipulate.
It is convenient both for decimal output and (via implicit coercion to
floating point) for low-precision arithmetic.  L<Math::Decimal> can be
used for high-precision arithmetic.  Its resolution is unlimited.

=cut

my $dec_last_dayno = "0";
my $dec_mn_s = "0";

sub now_tai_dec(;$) {
	my($dayno, $secs, $bound) = now_utc_dec($_[0]);
	if($dayno ne $dec_last_dayno) {
		$dec_mn_s = utc_to_tai(Math::BigRat->new($dayno), BIGRAT_ZERO)
				->bstr;
		$dec_last_dayno = $dayno;
	}
	return (dec_add($dec_mn_s, $secs), $bound);
}

=back

=head1 SEE ALSO

L<Time::TAI>,
L<Time::UTC::Now>

=head1 AUTHOR

Andrew Main (Zefram) <zefram@fysh.org>

=head1 COPYRIGHT

Copyright (C) 2006, 2009, 2010, 2017
Andrew Main (Zefram) <zefram@fysh.org>

=head1 LICENSE

This module is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut

1;
