1005 lines
42 KiB
Plaintext
1005 lines
42 KiB
Plaintext
/*
|
|
|
|
This file contains the implementation of Apollo Time, a cross-platform time implementation
|
|
that has both high precision and very long range. It provides a unified way of representing
|
|
time for most common software operations, and provides a common space in which times
|
|
can be easily compared (cleaning up the mess made by many operating systems, where your
|
|
wall clock time and file modification time are measured in different units).
|
|
|
|
Apollo Time is a 128-bit integer, defined as the number of femtoseconds since
|
|
the Apollo 11 Moon Landing. (There are 10**15 femtoseconds in one second.)
|
|
|
|
While having relatively high accuracy compared to most current time formats, Apollo Time also has
|
|
a very long range, able to represent times 5 quadrillion years in the past and future.
|
|
|
|
Because of this combination of high accuracy and long range, you can use Apollo_Time both
|
|
to represent the wall clock time, as well as short durations, like a small fraction of
|
|
a 240Hz frame that you might then use for physics simulation. You can use it to represent
|
|
times measured by specialized hardware that runs faster than your CPU -- general-purpose
|
|
CPUs in the year 2022 can only meaningfully ask what time it is in intervals of something
|
|
like 10**-9 seconds, and this has not changed in 20 years; but you might be interfacing
|
|
with a higher-resolution timer that runs independently of the CPU's clock and buffers
|
|
its results until the slow CPU can get them, or that uses interpolation to measure
|
|
the times of events to much greater accuracy. Thus the quantum of 10**-15 represents
|
|
a reasonable degree of future-proofing, though we do wonder if it is enough (the Planck
|
|
time is around 10**-44 seconds, so ... yeah. If we figure out how to make nanocomputers,
|
|
we might need more.)
|
|
|
|
This extra space also gives you the ability to perform math on Apollo_Times without
|
|
worrying too much about losing precision in intermediate results, as easily happens
|
|
with floating-point formats. Also, because this is an integer format, accuracy is
|
|
constant; it doesn't change depending on what time it is, as with floating-point formats.
|
|
So, Apollo_Time is suitable for programs that want to represent time in a way that is
|
|
easy to deal with but also rock solid.
|
|
|
|
|
|
Frequently Asked Questions:
|
|
|
|
Q: Isn't this overkill? Why not use a smaller time format with more reasonable range and accuracy?
|
|
|
|
A: The history of computing platforms to date is full of time formats that people decide
|
|
are probably maybe fine, that then cause problems only a short time later.
|
|
In Linux today we have time_t, timeval, timespec, and tm, and yet
|
|
still somehow there's a problem where we'll need to expend a great deal of effort to
|
|
revamp all this software before the year 2038, when it will stop working due to numerical
|
|
overflow (which maybe will attempt to be solved by expanding the sizes of these data
|
|
structures, which will break some programs). Other systems have problems that are similar
|
|
but different; when we want to deploy a robust program across all these systems,
|
|
we start drowning in all the details.
|
|
|
|
Shouldn't we have learned, by now, that we need a lot of range and precision in our
|
|
time variables? Instead of kicking the can down the road by adding slightly more information
|
|
each time, then running into more problems,
|
|
why not just hit the problem with a giant hammer and solve it for a very long time?
|
|
We have better things to do than debug timestamp problems.
|
|
|
|
If the cost of the solution were low and the amount of benefit were high, this would clearly
|
|
be the right decision. In this case it seems clearly to be true; timestamp data is a small
|
|
minority of the data stored by programs, and manipulations of this data are a very small
|
|
fraction of CPU cycles. So adding more overhead here, by doubling the size, has a small
|
|
cost in reality; but the benefit is big, because we remove a huge class of problems
|
|
and uncertainties.
|
|
|
|
Software today tends to be very buggy, and it tends not to do much more than software did
|
|
30 years ago. It is limited by complexity -- the programmers just have too much to do,
|
|
too many complex scenarios to think about, too many bugs to fix. To the extent we can
|
|
reduce this, we should, and software will be better for it (but it must be said that
|
|
a lot of this complexity is self-induced by programmers' lack of appropriate guiding
|
|
aesthetics).
|
|
|
|
(All this said, people who tell you how you should program are always saying stuff
|
|
like the previous paragraph. But this time it's true.)
|
|
|
|
In addition, for this programming language and the associated user-level code,
|
|
we have the goal of running the same across many platforms, where those platforms
|
|
have different time conventions. It is helpful to us to know that our time convention
|
|
is a superset of all of those, so that we can build interfaces that do not
|
|
create errors due to their time representations having less range than the
|
|
underlying implementations.
|
|
|
|
The goal here is just to clean up the BS that programmers already deal with today,
|
|
and also provide a reasonable amount of future-proofing. It's unclear that we could do a
|
|
good job of this in only 64 bits, and if we get bigger than that, 128 bits is the natural
|
|
size. 64 bits would be friendlier to CPUs of today (though this might not matter tomorrow);
|
|
but if we want gigahertz accuracy (seemingly a minimum since today's CPUs are measured in gigahertz),
|
|
we would only get 292 years of range in 64 bits. 128 bits just kills this problem dead,
|
|
and allows us to have much more accuracy.
|
|
|
|
(Even just for applications that start a timer at 0 and startup,
|
|
do you want to be writing programs that could only conceivably run for 292 years
|
|
without crashing? Does that seem like a good idea to anyone?)
|
|
|
|
|
|
Q: Why not use a different zero point, like trying to be more compatible with one of the already
|
|
established conventions, such as Unix's January 1, 1970 or Windows' January 1, 1601?
|
|
|
|
A: The choice of zero point is not arbitrary. It is an opportunity for celebration
|
|
and for aspiration, and we choose to take this opportunity rather than throw it away.
|
|
|
|
The author of these notes was born on November 3, 1971 (coincidentally, the day that
|
|
the first edition of Unix was released) and is, at the time of this writing, a little
|
|
over 50 years old; Apollo 11 happened before he was born, and for his entire life,
|
|
he has never seen a feat of technology that comes close to Apollo.
|
|
|
|
If you don't go to the moon for 5 to 10 years, maybe you just didn't want to, maybe
|
|
you felt it's a little too expensive, not now, I have a headache, and so forth.
|
|
But if this situation continues and you don't go to the moon for 2 generations of people,
|
|
that's a totally different thing.
|
|
|
|
At the current time the USA is no longer capable of landing on the moon, nor has it been
|
|
for almost my entire life.
|
|
If we are not in denial, we must admit that technology appears to have stagnated
|
|
or gone backwards in that area. People don't like to admit this, but when you peaked
|
|
50 years ago, I mean, come on.
|
|
All this said, there is an initiative toward moon and Mars landings that
|
|
appears strong, so hopefully we can recover.
|
|
|
|
In our view, software has undergone a similar period of stagnation. This programming
|
|
language and the accompanying user-level code, including Apollo_Time, is the best
|
|
effort we know how to make, with the meager resources at our disposal, to try to
|
|
contribute toward moving software technology forward again.
|
|
|
|
This brings us to the idea of using the Unix timestamp, which is pretty close in time
|
|
to the Apollo landing and seems cleaner because it starts at the beginning of a year.
|
|
Well, Unix was good at the time, but we should be moving past it; yet even
|
|
52 years later we seem to be unable, and this is deeply sad in some way.
|
|
(Meanwhile the Unix we have becomes worse over time, in that it becomes
|
|
tremendously more complicated while offering a relatively small amount of added
|
|
functionality.)
|
|
|
|
We would like to do something about all this. If we can.
|
|
|
|
"But why, some say, the Moon? Why choose this as our goal? And they may well ask,
|
|
why climb the highest mountain? Why, 35 years ago, fly the Atlantic?
|
|
Why does Rice play Texas?
|
|
|
|
We choose to go to the Moon in this decade and do the other things,
|
|
not because they are easy, but because they are hard;
|
|
because that goal will serve to organize and measure the best
|
|
of our energies and skills, because that challenge is one that we are
|
|
willing to accept, one we are unwilling to postpone, and one
|
|
we intend to win."
|
|
|
|
|
|
Q: Why is the landing of the lunar module used as the zero point for Apollo_Time,
|
|
rather than the launch of the rocket?
|
|
|
|
A: Because coffee is for closers.
|
|
|
|
The launch of the rocket represents an ambitious effort. And this kind of ambition
|
|
is important, and probably good to commemorate. But the landing of the lunar module
|
|
represents the success of the mission, rather than just the attempt. And success
|
|
is important; it's the only objective measure we have to help calibrate our beliefs --
|
|
if beliefs result in expected outcomes, maybe they are correct.
|
|
|
|
This is not a necessary or even sufficient condition; success and failure have
|
|
large random factors. But successful conclusion of engineering efforts is correlated to
|
|
accurate and self-consistent beliefs; if we lose our aesthetic for engineering success,
|
|
our beliefs become unmoored.
|
|
|
|
Unfortunately, across Western societies, we have been changing our attitude
|
|
toward engineering success, in a way that pretends reality is subservient
|
|
to our social system. Community and inclusion and good feelings are valued
|
|
more than correctness. We pretend that the discipline of software engineering
|
|
is somehow more about identifying as a programmer, and wanting to contribute
|
|
positively to a community, than it is about the quality of the resulting code,
|
|
or indeed any measurable characteristics of the resulting system.
|
|
We pretend that engineering success comes *because* someone
|
|
identifies as an engineer inside a community, and that person
|
|
showed up and performed the requisite social signals of doing engineering.
|
|
|
|
This new aesthetic has been
|
|
adopted widely without serious consideration for the damage it may do to our ability
|
|
to solve problems. So if we now observe that we are no longer good at solving
|
|
problems, maybe it's not that surprising.
|
|
|
|
Good feelings are nice and there's nothing wrong with enjoying your participation
|
|
in the engineering community. But when it comes to the question of how much thrust
|
|
to apply in what direction at what time for the spaceship not to crash into the moon,
|
|
there simply are right answers and wrong answers, and getting a high-quality solution is
|
|
much more important than enjoying your social identity as an engineer or indulging
|
|
your desire to feel smart.
|
|
|
|
|
|
Q: Why not the time of first foot on the ground, rather than the landing of the module?
|
|
Isn't that the real success?
|
|
|
|
A: Aesthetic fine-tuning. Boots-on-ground feels like it pushes things into more of an ego direction,
|
|
being more about one person or one country. The moon landing is an achievement for humanity,
|
|
that any human being on Earth can use as inspiration.
|
|
|
|
|
|
NB. In Vernor Vinge's novel _A Deepness in the Sky_, the use of the moon landing
|
|
as the 0-point of time measurement is mentioned, though it is then revealed that
|
|
the actual 0-point is 15 million seconds later, in other words, it is the
|
|
Unix time. In that book, software is described as an accretion of
|
|
many layers over thousands of years, and "computer archaeologists" are needed
|
|
in order to do things with the deeper layers of the system. Whereas this is
|
|
a little bit funny, knowing what we know about software, we do not see this
|
|
as a viable model for the future: if we do things this way, it is unlikely
|
|
that we will be able to have cool space ships zooming around the galaxy.
|
|
But, the book's observation that the moon landing is pretty close to the Unix time,
|
|
and the idea that maybe the moon landing was the actual time, probably helped
|
|
generate the idea for the code in this file.
|
|
|
|
*/
|
|
|
|
/*
|
|
@Incomplete:
|
|
lerp
|
|
If it greatly facilitates calendar_to_apollo, add day_of_year_starting_at_0 to the struct (analogous to the Unix one), but then clarify which members are used for conversion and which can be left empty.
|
|
*/
|
|
|
|
|
|
|
|
// We do not currently have 128-bit integers, and may never (?), so we are defining Apollo_Time
|
|
// as a struct.
|
|
|
|
// Apollo_Time represents a time in objective reality. It does not get adjusted based on what time zone
|
|
// you're in. Adding and subtracting hours to Apollo_Time is not the right way to implement time zones,
|
|
// and if you do this, you are inviting weird bugs and generally asking for trouble.
|
|
// Time zones are not a property of time passing in objective reality; they are a choice we make
|
|
// when we describe what time it is. We have another struct, Calendar_Time, that is meant to help
|
|
// describe time in ways that humans find convenient. You can convert from Apollo_Time to Calendar_Time,
|
|
// and when you do this, you can supply a time zone for the conversion.
|
|
|
|
// The same goes for leap seconds and other calendar adjustments. These are not properties of time
|
|
// passing in the physical world, so Apollo_Time does not know or care about them. Apollo_Time is very simple.
|
|
// Leap seconds, day skips, and so forth, are aspects of Calendar_Time only.
|
|
|
|
|
|
// Note, if we establish a more accurate time for the moon landing, then we may adjust the baseline time
|
|
// during the beta. Anticipate that this might happen (of course it won't change by much!)
|
|
|
|
// Eventually this will replace the stuff in modules/Time. Maybe this will become modules/Time?
|
|
|
|
Apollo_Time :: #type,isa S128;
|
|
|
|
// We reserve the maximum negative value of Apollo Time, which (in the regular
|
|
// two's-complement way has no corresponding positive value), to be an invalid
|
|
// time that you can use to represent a field that is unset:
|
|
|
|
APOLLO_TIME_INVALID :: Apollo_Time.{0, 0x8000_0000_0000_0000};
|
|
|
|
// We did not make this the default value of Apollo_Time, though, because it's
|
|
// not totally clear that this is the best move. But maybe we should.
|
|
|
|
|
|
// Here is some stuff for dealing with calendar times:
|
|
|
|
|
|
Time_Zone :: enum u32 {
|
|
UTC :: 0;
|
|
LOCAL :: 1; // Whatever the system local time zone is.
|
|
|
|
// Maybe we will extend this with other fixed zones.
|
|
}
|
|
|
|
// Musa's shitty macros:
|
|
calendar_to_file_timestamp :: (using ct: Calendar_Time) -> string {
|
|
date_part :: #bake_arguments formatInt(minimum_digits = 2);
|
|
time_string := sprint("%.%.%_%.%.%", year, date_part(month_starting_at_0 + 1), date_part(day_of_month_starting_at_0 + 1), date_part(hour), date_part(minute), date_part(second));
|
|
return time_string;
|
|
}
|
|
|
|
format_datetime :: (at: Apollo_Time) -> string {
|
|
current_time := to_calendar(at, time_zone=.LOCAL);
|
|
// Alternatives:
|
|
// return calendar_to_string(current_time);
|
|
// return calendar_to_iso_string(current_time);
|
|
return calendar_to_file_timestamp(current_time);
|
|
} // #rename this!
|
|
|
|
Calendar_Time :: struct {
|
|
year: s32;
|
|
month\ _starting_at_0: s8; // 0-11. Use as an index to a month name array, or add 1 to get the normal human-readable number.
|
|
day_of_month_starting_at_0: s8; // 0-30. Add 1 to get the normal human-readable number.
|
|
day_of_week\_starting_at_0: s8; // Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday.
|
|
hour: s8;
|
|
minute: s8;
|
|
second: s8;
|
|
millisecond: s16;
|
|
|
|
time_zone: Time_Zone;
|
|
}
|
|
|
|
// Why are those members of Calendar_Time named verbosely with _starting_at_0?
|
|
// Because on different OSs, these all have different conventions, that are inconsistent
|
|
// not only with each other, but internally as well. We put the base of each variable
|
|
// in the name so that you are reminded of this every time you use it, which will hopefully
|
|
// make use of this kind of struct less error-prone.
|
|
|
|
|
|
//
|
|
// When it comes to asking what time it is right now, there are two basic functions
|
|
// that are designed in an operating-system-specific way (so they are further down
|
|
// in the file, in the OS-specific section). These are:
|
|
//
|
|
// current_time_consensus() -> Apollo_Time
|
|
// current_time_monotonic() -> Apollo_Time
|
|
//
|
|
// This is because there are two different concepts of time that we need today in software,
|
|
// and mixing them up leads to a lot of confusions and problems and bugs.
|
|
//
|
|
// The way things still work in 2022, your computer has an internal timer that it uses
|
|
// to advance its own local concept of time in some mostly correct way. But
|
|
// there are inaccuracies, so if you have a bunch of computers in the world, eventually
|
|
// their idea of time will drift apart from each other due to compounded small errors.
|
|
// So we synchronize times over the network; somewhere we have computers that we trust
|
|
// more, and they tell us what time it really is, and if our local computer is off
|
|
// by a little bit, we correct the error and move on.
|
|
//
|
|
// But this correction step means that if you want to know what time it is, according
|
|
// to this consensus, then in rare cases it can skip forward, or even go backward.
|
|
// That's not a very good thing if you are trying to run a realtime program that is
|
|
// simulating forward a world, or some UI animation transitions, in sync with real time.
|
|
// If that program asks what time it is on frame N, and then again on frame N+1, and
|
|
// subtracts those times to know how much to simulate forward, and it gets a negative
|
|
// result, this is very confusing and bad and it doesn't know what to do. (And even
|
|
// when results are positive, they may jitter substantially with respect to realtime,
|
|
// whenever corrections to the consensus time happen). This is why we have a thing
|
|
// called monotonic time, which is guaranteed to never decrease (and in general to
|
|
// increase as smoothly as possible relative to the average passage of time).
|
|
//
|
|
// So, if you want to get a time value that you wish to convert to a readable
|
|
// calendar time that users can see, use current_time_consensus().
|
|
//
|
|
// If you want to get a time that you can use for simulation, use current_time_monotonic().
|
|
//
|
|
// Consensus time will make sense to a human when you call to_calendar() on it.
|
|
// Monotonic time may or may not, depending on the implementation, so don't rely on that.
|
|
// This suggests that we may want to have more typesafety on Apollo_Time, or maybe even
|
|
// dedicate a bit to representing which of these we are talking about, to help you spot
|
|
// errors. But that's not how it is right now, so, just look out for this.
|
|
//
|
|
|
|
|
|
ConvertToApollo :: (m: s64, FACTOR: s64) #expand {
|
|
result: Apollo_Time = ---;
|
|
|
|
#if CPU == .X64 { // Maybe ditch this and simply always use the Int128 routine?
|
|
#asm {
|
|
result_high: gpr === d;
|
|
result_low: gpr === a;
|
|
|
|
mov result_low, FACTOR;
|
|
imul result_high, result_low, m;
|
|
|
|
mov [*result + 0], result_low;
|
|
mov [*result + 8], result_high;
|
|
}
|
|
} else {
|
|
result = cast(Apollo_Time)(to_s128(m) * FACTOR);
|
|
}
|
|
|
|
`return result;
|
|
}
|
|
|
|
seconds_to_apollo :: (m: s64) -> Apollo_Time {
|
|
// seconds are 10**0, Apollo_Time is 10**-15.
|
|
// So we multiply by 10**15.
|
|
ConvertToApollo(m, 1_000_000_000_000_000);
|
|
}
|
|
|
|
milliseconds_to_apollo :: (m: s64) -> Apollo_Time {
|
|
// milliseconds are 10**-3, Apollo_Time is 10**-15.
|
|
// So we multiply by 10**12.
|
|
ConvertToApollo(m, 1_000_000_000_000);
|
|
}
|
|
|
|
microseconds_to_apollo :: (m: s64) -> Apollo_Time {
|
|
// microseconds are 10**-6, Apollo_Time is 10**-15.
|
|
// So we multiply by 10**9.
|
|
ConvertToApollo(m, 1_000_000_000);
|
|
}
|
|
|
|
nanoseconds_to_apollo :: (m: s64) -> Apollo_Time {
|
|
// nanoseconds are 10**-9, Apollo_Time is 10**-15.
|
|
// So we multiply by 10**6.
|
|
ConvertToApollo(m, 1_000_000);
|
|
}
|
|
|
|
ConvertFromApollo :: inline (t: Apollo_Time, divisor: u64, $DO_ROUNDING: bool) -> (result: s64, success: bool) {
|
|
quotient, remainder, overflow := signed_divide_with_remainder(t, S128.{divisor, 0});
|
|
|
|
// The quotient may be too large to store in an s64: for valid values quotient.high will only be
|
|
// zero (positive result) or full (negative result); if it's anything else the value is extending
|
|
// into the high bits.
|
|
overflow ||= !(quotient.high == 0 || quotient.high == 0xffffffff_ffffffff);
|
|
|
|
// The low bits are already stored in two's-complement - we don't need to do anything to negate them!
|
|
// However, see later...
|
|
result := cast,no_check(s64) quotient.low;
|
|
|
|
// result is the output of integer division; that is, it is how many times divisor fits inside t.
|
|
// If divisor is not a factor of t then remainder will be > 0, and you can think of result as if it were
|
|
// real division truncated to the nearest integer toward 0: if t is positive then result will
|
|
// be lower than the exact real value, if t is negative then result will be greater than the exact real
|
|
// value. Therefor for rounding, when positive we are choosing either result or result+1, and for
|
|
// negative we are choosing either result or result-1.
|
|
#if DO_ROUNDING {
|
|
// remainder will be in range 0 to divisor-1; remainder and divisor are both positive!
|
|
// The decision we use to round is whether remainder is greater than divisor / 2; if it is then
|
|
// we need to change result to the value further away from 0.
|
|
// Note that because remainder < divisor (a u64), remainder.high is always 0 and we can ignore it.
|
|
half_divisor_floored := divisor / 2;
|
|
if t.high >= 0 {
|
|
// When t is positive we also want to round up when remainder is exactly half divisor
|
|
if remainder.low > half_divisor_floored
|
|
|| divisor % 2 == 0 && remainder.low == half_divisor_floored
|
|
result += 1;
|
|
} else {
|
|
// When t is negative the rouded-up value is the default, so we don't need to check for equality.
|
|
if remainder.low > half_divisor_floored
|
|
result -= 1;
|
|
}
|
|
}
|
|
|
|
// ...It is possible for result to be wrong; we may have overflowed. This is easy to detect because
|
|
// divisor is a u64 - always positive - so a valid result will always be the same sign as t.
|
|
// Therefor, if we have a sign difference between t and result we know we've overflowed.
|
|
overflow ||= result != 0 && ((t.high < 0) != (result < 0));
|
|
|
|
return result, !overflow;
|
|
}
|
|
|
|
// The following routines return success==true unless the Apollo_Time quantity
|
|
// is too big to fit into 'result'.
|
|
|
|
to_seconds :: (t: Apollo_Time, $DO_ROUNDING := false) -> (result: s64, success: bool) {
|
|
// seconds are 10**0, Apollo_Time is 10**-15.
|
|
// So we divide by 10**15.
|
|
result, success := ConvertFromApollo(t, 1_000_000_000_000_000, DO_ROUNDING);
|
|
return result, success;
|
|
}
|
|
|
|
to_milliseconds :: (t: Apollo_Time, $DO_ROUNDING := false) -> (result: s64, success: bool) {
|
|
// milliseconds are 10**-3, Apollo_Time is 10**-15.
|
|
// So we divide by 10**12.
|
|
result, success := ConvertFromApollo(t, 1_000_000_000_000, DO_ROUNDING);
|
|
return result, success;
|
|
}
|
|
|
|
to_microseconds :: (t: Apollo_Time, $DO_ROUNDING := false) -> (result: s64, success: bool) {
|
|
// microseconds are 10**-6, Apollo_Time is 10**-15.
|
|
// So we divide by 10**9.
|
|
result, success := ConvertFromApollo(t, 1_000_000_000, DO_ROUNDING);
|
|
return result, success;
|
|
}
|
|
|
|
to_nanoseconds :: (t: Apollo_Time, $DO_ROUNDING := false) -> (result: s64, success: bool) {
|
|
// nanoseconds are 10**-9, Apollo_Time is 10**-15.
|
|
// So we divide by 10**6.
|
|
result, success := ConvertFromApollo(t, 1_000_000, DO_ROUNDING);
|
|
return result, success;
|
|
}
|
|
|
|
|
|
calendar_to_string_time_then_date :: (ct: Calendar_Time, use_24_hour_time:=true) -> string /* Temporary_Storage */ {
|
|
// For now we just do English. We can explore how to internationalize this later,
|
|
// as well as provide different formats (numeric, wordy, abbreviated, etc).
|
|
// Probably once things get complicated like that, it should live in another file,
|
|
// and this here should just be about the numeric parts of the time.
|
|
|
|
month_names :: string.["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
|
|
month := ct.month_starting_at_0;
|
|
month = clamp(month, 0, 11);
|
|
|
|
month_name := month_names[month];
|
|
|
|
{
|
|
using ct;
|
|
|
|
Clock :: (x: Any) -> FormatInt {
|
|
return formatInt(x, minimum_digits=2);
|
|
}
|
|
|
|
|
|
if use_24_hour_time {
|
|
return tprint("%:%:%, % % %",
|
|
Clock(hour), Clock(minute), Clock(second),
|
|
day_of_month_starting_at_0+1, month_name, year
|
|
);
|
|
} else {
|
|
hour12 := hour % 12;
|
|
if hour12 == 0 hour12 = 12;
|
|
ampm := ifx (hour < 12) then "AM" else "PM";
|
|
return tprint("%:%:% %, % % %",
|
|
Clock(hour12), Clock(minute), Clock(second), ampm,
|
|
day_of_month_starting_at_0+1, month_name, year
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
calendar_to_string :: (ct: Calendar_Time, use_24_hour_time:=true) -> string /* Temporary_Storage */ {
|
|
// For now we just do English. We can explore how to internationalize this later,
|
|
// as well as provide different formats (numeric, wordy, abbreviated, etc).
|
|
// Probably once things get complicated like that, it should live in another file,
|
|
// and this here should just be about the numeric parts of the time.
|
|
|
|
month_names :: string.["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
|
|
month := ct.month_starting_at_0;
|
|
month = clamp(month, 0, 11);
|
|
|
|
month_name := month_names[month];
|
|
|
|
{
|
|
using ct;
|
|
|
|
Clock :: (x: Any) -> FormatInt {
|
|
return formatInt(x, minimum_digits=2);
|
|
}
|
|
|
|
|
|
if use_24_hour_time {
|
|
return tprint("% % %, %:%:%", day_of_month_starting_at_0+1, month_name, year, Clock(hour), Clock(minute), Clock(second));
|
|
} else {
|
|
hour12 := hour % 12;
|
|
if hour12 == 0 hour12 = 12;
|
|
ampm := ifx (hour < 12) then "AM" else "PM";
|
|
return tprint("% % %, %:%:% %", day_of_month_starting_at_0+1, month_name, year, Clock(hour12), Clock(minute), Clock(second), ampm);
|
|
}
|
|
}
|
|
}
|
|
|
|
calendar_to_iso_string :: (ct: Calendar_Time) -> string /* Temporary_Storage */ {
|
|
using ct;
|
|
|
|
date_part :: #bake_arguments formatInt(minimum_digits = 2);
|
|
|
|
timezone_name := ifx ct.time_zone == .UTC then "Z" else ""; // @Incomplete: Maybe determine local time zone name or offset
|
|
|
|
time_string := tprint("%-%-%T%:%:%.%0%", year, date_part(month_starting_at_0 + 1), date_part(day_of_month_starting_at_0 + 1), date_part(hour), date_part(minute), date_part(second), formatInt(millisecond, minimum_digits = 3), timezone_name);
|
|
return time_string;
|
|
}
|
|
// #run {
|
|
// time: Apollo_Time;
|
|
// cal := to_calendar(time);
|
|
// time_string := calendar_to_iso_string(cal);
|
|
// assert(time_string == "1969-07-20T20:17:39.000Z", "Unexpected time string: %", time_string);
|
|
// }
|
|
|
|
|
|
compare_apollo_times :: (a: Apollo_Time, b: Apollo_Time) -> s32 {
|
|
// If you want to pass this to a two-op compare function.
|
|
// Probably we should fix some stuff about this language so you don't need this?
|
|
|
|
if a < b return -1;
|
|
if a == b return 0;
|
|
return 1;
|
|
}
|
|
|
|
to_float64_seconds :: (input: Apollo_Time) -> (result: float64, success: bool) {
|
|
// @Incomplete @Robustness:
|
|
// Not sure what the best strategy is here. We probably should manually convert,
|
|
// like we do in to_apollo(), but
|
|
// as an intermediate low-effort thing I am going to convert each word separately to
|
|
// float64 and then add them.
|
|
|
|
a := input;
|
|
if input.high < 0 a = -a; // Do this on a positive number and re-negate at the end.
|
|
|
|
seconds, success := to_seconds(a, DO_ROUNDING=false); // @Robustness: Note that our 'seconds' is signed which means we are missing 1 bit of range.
|
|
if !success return 0, false;
|
|
|
|
f := cast(float64) seconds;
|
|
|
|
b := seconds_to_apollo(seconds);
|
|
femto := a - b;
|
|
|
|
assert(femto.high == 0); // This requires the result of to_seconds not to be rounded.
|
|
f_femto := femto.low / cast(float64)FEMTOSECONDS;
|
|
|
|
f += f_femto;
|
|
|
|
if input.high < 0 f = -f;
|
|
return f, true;
|
|
}
|
|
|
|
to_apollo :: (seconds: float64) -> Apollo_Time {
|
|
MANTISSA_BITS :: 52;
|
|
SIGN_BIT :: 0x8000_0000_0000_0000;
|
|
EXPONENT_MASK :: 0x7ff0_0000_0000_0000;
|
|
|
|
i64 := (.*) cast(*u64) *seconds;
|
|
exponent := cast(s16)((i64 & EXPONENT_MASK) >> 52);
|
|
exponent -= 1023;
|
|
exponent -= MANTISSA_BITS; // Not treating this as a decimal, just an integer.
|
|
|
|
MANTISSA_HIGH_BIT :: 1 << MANTISSA_BITS;
|
|
mantissa := (i64 & (MANTISSA_HIGH_BIT-1)) | MANTISSA_HIGH_BIT;
|
|
|
|
if !mantissa {
|
|
a: Apollo_Time;
|
|
return a; // Zero.
|
|
}
|
|
|
|
a := seconds_to_apollo(xx mantissa); // In the current format, seconds start in the low word, so we know we have more than the mantissa size available.
|
|
|
|
shift := exponent;
|
|
if shift < 0 {
|
|
shift = -shift;
|
|
if shift > 255 return .{};
|
|
|
|
a >>= cast(u8)(shift);
|
|
} else if shift {
|
|
if shift > 255 return .{};
|
|
a <<= cast(u8) shift;
|
|
}
|
|
|
|
return a;
|
|
}
|
|
|
|
//
|
|
// Musa's helper macros & functions
|
|
//
|
|
|
|
is_valid :: inline (a: Apollo_Time) -> bool {
|
|
return (a != .{0, 0}) && (a != APOLLO_TIME_INVALID);
|
|
}
|
|
|
|
time_has_elapsed :: (first_time: Apollo_Time, current_time: Apollo_Time, time_elapsed_seconds: float64) -> bool /*#expand*/ {
|
|
if !is_valid(first_time) || !is_valid(current_time) return false;
|
|
|
|
first_time_seconds := to_float64_seconds(first_time);
|
|
current_time_seconds := to_float64_seconds(current_time);
|
|
|
|
return first_time_seconds + time_elapsed_seconds < current_time_seconds;
|
|
}
|
|
|
|
time_remaining_seconds :: (first_time: Apollo_Time, current_time: Apollo_Time, time_elapsed_seconds: float64) -> float64 #expand {
|
|
if !is_valid(first_time) || !is_valid(current_time) return FLOAT64_MAX;
|
|
|
|
return (to_float64_seconds(first_time) + time_elapsed_seconds) - to_float64_seconds(current_time);
|
|
}
|
|
|
|
//
|
|
// Windows stuff:
|
|
//
|
|
|
|
#if OS == .WINDOWS {
|
|
#scope_file
|
|
|
|
// Instead of importing Windows, we could just locally define the small set
|
|
// of bindings here that we actually use. Maybe a good project would be to provide
|
|
// a Windows module that is divided into little pieces of what you might
|
|
// commonly use.
|
|
W :: #import "Windows";
|
|
FILETIME :: W.FILETIME;
|
|
|
|
// You can compute filetime_apollo by uncommenting and calling get_apollo_moon_landing_windows_filetime().
|
|
filetime_apollo :: FILETIME.{
|
|
dwLowDateTime = 0x99a4_7380,
|
|
dwHighDateTime = 0x19d_30e0
|
|
};
|
|
|
|
// And this is computed by get_january_1_1601_apollo_time():
|
|
apollo_filetime :: Apollo_Time.{12544592398102855680, -630480};
|
|
|
|
// FILETIME is in 100-nanosecond increments, i.e. 10**-7.
|
|
// Apollo_Time is 10**-15.
|
|
// So we multiply by 10**8.
|
|
FILETIME_FACTOR :: 100_000_000;
|
|
|
|
#scope_export
|
|
|
|
current_time_consensus :: () -> Apollo_Time {
|
|
ft_now: FILETIME;
|
|
W.GetSystemTimePreciseAsFileTime(*ft_now);
|
|
|
|
apollo := filetime_to_apollo(ft_now);
|
|
return apollo;
|
|
}
|
|
|
|
current_time_monotonic :: () -> Apollo_Time {
|
|
pc, freq: W.LARGE_INTEGER;
|
|
|
|
// In principle QPC/QPF can fail, in that they return 'success',
|
|
// but in reality they don't. We don't want to dirty our API by
|
|
// passing this success code upward. Maybe this is a mistake,
|
|
// but causing all user programs to worry about what happens if they
|
|
// can't get a monotonic time seems weird and friction-inducing.
|
|
// If we get false back, we could fail over to rdtscp or something.
|
|
|
|
success := W.QueryPerformanceCounter(*pc);
|
|
|
|
// We could try to cache the frequency somewhere, but
|
|
// it seems like this may be slower than asking for it again,
|
|
// in a multithreaded environment. Hard to say. Should ask around.
|
|
// -jblow, 9 March 2022.
|
|
|
|
success = W.QueryPerformanceFrequency(*freq);
|
|
|
|
a: Apollo_Time;
|
|
a.low = cast,no_check(u64)pc.QuadPart;
|
|
|
|
a *= FEMTOSECONDS;
|
|
a /= freq.QuadPart;
|
|
|
|
return a;
|
|
}
|
|
|
|
filetime_to_apollo :: (ft: FILETIME) -> Apollo_Time {
|
|
FILETIME_FACTOR :: 100_000_000;
|
|
|
|
a: Apollo_Time = ---;
|
|
a.low = (.*)cast(*u64)*ft;
|
|
a.high = 0;
|
|
|
|
a *= FILETIME_FACTOR;
|
|
a += apollo_filetime;
|
|
|
|
return a;
|
|
}
|
|
|
|
to_filetime :: (at: Apollo_Time) -> (ft: FILETIME, success: bool) {
|
|
result, success := ConvertFromApollo(at, FILETIME_FACTOR, true);
|
|
|
|
result1 := cast(u64) result;
|
|
result += xx filetime_apollo.QuadPart;
|
|
|
|
// @Incomplete: Check for overflow results due to the filetime_apollo add?
|
|
|
|
ft: FILETIME = ---;
|
|
ft.QuadPart = cast(u64) result;
|
|
return ft, success;
|
|
}
|
|
|
|
to_calendar :: (at: Apollo_Time, time_zone := Time_Zone.UTC) -> Calendar_Time {
|
|
// :OSSpecific
|
|
// @Incomplete: We would like to do this via our own code
|
|
// that is cross-platform and guaranteed to be the same everywhere,
|
|
// etc, but for now we are falling back
|
|
// on OS-specific routines in order to get things going.
|
|
// Since our own routine probably will not have failure modes (?), we are not for now
|
|
// trying to detect and return failure.
|
|
|
|
ft, success := to_filetime(at);
|
|
if !success log("Error: Input Apollo_Time was out of range when calling to_calendar(). We are not putting failure into the API though, because long-term in an OS-independent scenario we should not have this kind of failure case, maybe.\n");
|
|
|
|
if time_zone == .LOCAL {
|
|
ft_local: FILETIME;
|
|
success := W.FileTimeToLocalFileTime(*ft, *ft_local);
|
|
assert(success != .FALSE);
|
|
if success ft = ft_local;
|
|
}
|
|
|
|
st: W.SYSTEMTIME;
|
|
st_success := W.FileTimeToSystemTime(*ft, *st);
|
|
assert(st_success != .FALSE); // Under what conditions does this fail? @Incomplete @Robustness.
|
|
|
|
ct: Calendar_Time;
|
|
ct.year = st.wYear;
|
|
ct.month_starting_at_0 = cast,trunc(s8) Max(0, st.wMonth - 1); // @Temporary: Guarding against fails and getting a 0 back for st.wMonth then returning a negative number.
|
|
ct.day_of_month_starting_at_0 = cast,trunc(s8) Max(0, st.wDay-1); // @Temporary
|
|
ct.day_of_week_starting_at_0 = cast,trunc(s8) st.wDayOfWeek;
|
|
ct.hour = cast,trunc(s8) st.wHour;
|
|
ct.minute = cast,trunc(s8) st.wMinute;
|
|
ct.second = cast,trunc(s8) st.wSecond;
|
|
ct.millisecond = cast,trunc(s16) st.wMilliseconds;
|
|
|
|
ct.time_zone = time_zone;
|
|
|
|
return ct;
|
|
}
|
|
|
|
calendar_to_apollo :: (ct: Calendar_Time) -> Apollo_Time {
|
|
// @Incomplete. See :OSSpecific
|
|
result: Apollo_Time = ---;
|
|
|
|
st: W.SYSTEMTIME;
|
|
st.wYear = xx,trunc ct.year;
|
|
st.wMonth = xx,trunc (ct.month\ _starting_at_0 + 1);
|
|
st.wDay = xx,trunc (ct.day_of_month_starting_at_0 + 1);
|
|
st.wDayOfWeek = xx,trunc ct .day_of_week\_starting_at_0;
|
|
st.wHour = xx,trunc ct.hour;
|
|
st.wMinute = xx,trunc ct.minute;
|
|
st.wSecond = xx,trunc ct.second;
|
|
st.wMilliseconds = xx,trunc ct.millisecond;
|
|
|
|
ft: FILETIME;
|
|
success := W.SystemTimeToFileTime(*st, *ft);
|
|
assert(success != .FALSE); // @Temporary
|
|
|
|
if ct.time_zone == .LOCAL {
|
|
ft_local: FILETIME;
|
|
success := W.LocalFileTimeToFileTime(*ft, *ft_local);
|
|
if success ft = ft_local;
|
|
assert(success != .FALSE);
|
|
}
|
|
|
|
apollo := filetime_to_apollo(ft);
|
|
return apollo;
|
|
}
|
|
|
|
/*
|
|
get_apollo_moon_landing_windows_filetime :: () -> FILETIME {
|
|
st_apollo: SYSTEMTIME;
|
|
{
|
|
// Sunday July 20, 1969 at 20:17:39 UTC. (Wikipedia says :40, other sources say :39).
|
|
|
|
using st_apollo;
|
|
wYear = 1969;
|
|
wMonth = 7; // July. Months in Windows SYSTEMTIME start at 1 even though wDayOfWeek starts at 0. Go figure.
|
|
wDayOfWeek = 0; // Sunday. Probably not necessary?
|
|
wDay = 20; // wDay starts at 1.
|
|
wHour = 20; // wHour starts at 0.
|
|
wMinute = 17;
|
|
wSecond = 39;
|
|
wMilliseconds = 0; // We don't have a more accurate number for this.
|
|
}
|
|
|
|
ft_apollo: FILETIME;
|
|
success := SystemTimeToFileTime(*st_apollo, *ft_apollo);
|
|
assert(success != .FALSE);
|
|
|
|
return ft_apollo;
|
|
}
|
|
|
|
get_january_1_1601_apollo_time :: () -> Apollo_Time {
|
|
ft0: FILETIME;
|
|
ft0a := filetime_to_apollo(ft0);
|
|
|
|
ca := to_calendar(ft0a); // Print this just to double-check.
|
|
print("ca: %\n", ca);
|
|
|
|
return ft0a;
|
|
}
|
|
*/
|
|
} else #if (OS == .LINUX || OS == .MACOS || OS == .ANDROID) {
|
|
#scope_file;
|
|
|
|
#import "POSIX";
|
|
|
|
|
|
libc :: #library,system "libc";
|
|
timelocal :: (timeptr: *tm) -> time_t #foreign libc;
|
|
timegm :: (timeptr: *tm) -> time_t #foreign libc;
|
|
|
|
#scope_export;
|
|
|
|
current_time_consensus :: () -> Apollo_Time {
|
|
ts: timespec;
|
|
clock_gettime(.REALTIME, *ts);
|
|
|
|
apollo := timespec_to_apollo(ts);
|
|
return apollo;
|
|
}
|
|
|
|
current_time_monotonic :: () -> Apollo_Time {
|
|
ts: timespec;
|
|
clock_gettime(.MONOTONIC_RAW, *ts);
|
|
|
|
apollo := timespec_to_apollo(ts);
|
|
return apollo;
|
|
}
|
|
|
|
timespec_to_apollo :: (ts: timespec) -> Apollo_Time {
|
|
a_seconds := seconds_to_apollo(ts.tv_sec);
|
|
a_nanoseconds := nanoseconds_to_apollo(ts.tv_nsec);
|
|
|
|
apollo := a_seconds + a_nanoseconds + APOLLO_TIME_FOR_JAN_1_1970;
|
|
return apollo;
|
|
}
|
|
|
|
to_timespec :: (at: Apollo_Time) -> (ts: timespec, success: bool) {
|
|
a := at - APOLLO_TIME_FOR_JAN_1_1970;
|
|
|
|
seconds: s64 = ---;
|
|
|
|
divided := a / FEMTOSECONDS;
|
|
if divided.high == 0 {
|
|
// We have a small enough number of seconds to fit into 64 bits.
|
|
// But we actually want 63 bits, so check the high bit of 'low'.
|
|
if divided.low & 0x8000_0000_0000_0000 {
|
|
return .{}, false; // Out of range.
|
|
} else {
|
|
seconds = cast(s64) divided.low;
|
|
}
|
|
} else if divided.high == -1 {
|
|
// This is a negative number with all the information in the low bits, so that's fine.
|
|
seconds = cast,no_check(s64) divided.low;
|
|
} else {
|
|
// There's information in the high bits, i.e. the result number of seconds
|
|
// is too big for the timespec range. Return false.
|
|
|
|
return .{}, false;
|
|
}
|
|
|
|
a_seconds := seconds_to_apollo(seconds);
|
|
|
|
a_femtoseconds := a - a_seconds;
|
|
assert(a_femtoseconds.high == 0);
|
|
|
|
nanoseconds: s64 = (cast(s64) a_femtoseconds.low + 500_000) / 1_000_000; // The number of femtoseconds in 1 second cannot be high enough such that the sign bit would be set after the cast.
|
|
|
|
ts: timespec = ---;
|
|
ts.tv_sec = xx,trunc seconds;
|
|
ts.tv_nsec = xx,trunc nanoseconds;
|
|
|
|
if seconds < 0 return ts, false; // timespec does not go negative.
|
|
return ts, true;
|
|
}
|
|
|
|
to_calendar :: (at: Apollo_Time, time_zone := Time_Zone.UTC) -> Calendar_Time {
|
|
// :OSSpecific @Incomplete
|
|
// Not returning a success code for the same reason as in the Windows version.
|
|
// (Read that comment to see.)
|
|
|
|
ts, success := to_timespec(at);
|
|
if !success {
|
|
log("Error: Input Apollo_Time was out of range when calling to_calendar(). We are not putting failure into the API though, because long-term in an OS-independent scenario we should not have this kind of failure case, maybe.\n");
|
|
return .{};
|
|
}
|
|
|
|
|
|
NANO_TO_MILLI_FACTOR :: 1_000_000;
|
|
milliseconds := cast,trunc(s16)((ts.tv_nsec + NANO_TO_MILLI_FACTOR/2) / NANO_TO_MILLI_FACTOR);
|
|
if milliseconds >= 1000 {
|
|
// The rounding above may have given us a result
|
|
// of 1000 milliseconds, which would be incorrect to return! So we need to add to
|
|
// the seconds, but we must do this *before* converting from timespec, because if
|
|
// we do it after, then we maybe have to increment minutes, hours, etc, which
|
|
// would be a big mess.
|
|
|
|
assert(milliseconds == 1000);
|
|
milliseconds = 0;
|
|
ts.tv_sec += 1;
|
|
}
|
|
|
|
|
|
// Wow, what a garbage. There almost certainly is a better way of doing this
|
|
// but I couldn't figure it out from browsing the Web because nobody there knows either.
|
|
tmp: tm;
|
|
if time_zone == .LOCAL {
|
|
tmp = localtime_r(*ts.tv_sec, *tmp);
|
|
} else {
|
|
tmp = gmtime_r(*ts.tv_sec, *tmp);
|
|
}
|
|
|
|
ct: Calendar_Time;
|
|
ct.year = tmp.tm_year + 1900;
|
|
ct.month_starting_at_0 = xx,trunc tmp.tm_mon;
|
|
ct.day_of_month_starting_at_0 = xx,trunc Max(0, tmp.tm_mday-1); // @Temporary
|
|
ct.day_of_week_starting_at_0 = cast,trunc(s8) tmp.tm_wday;
|
|
ct.hour = xx,trunc tmp.tm_hour;
|
|
ct.minute = xx,trunc tmp.tm_min;
|
|
ct.second = xx,trunc tmp.tm_sec;
|
|
ct.millisecond = milliseconds;
|
|
|
|
ct.time_zone = time_zone;
|
|
|
|
return ct;
|
|
}
|
|
|
|
calendar_to_apollo :: (ct: Calendar_Time) -> Apollo_Time {
|
|
// @Incomplete. See :OSSpecific
|
|
|
|
using parts: tm;
|
|
tm_year = ct.year - 1900;
|
|
tm_mon = ct.month_starting_at_0;
|
|
tm_mday = ct.day_of_month_starting_at_0 + 1;
|
|
tm_hour = ct.hour;
|
|
tm_min = ct.minute;
|
|
tm_sec = ct.second;
|
|
tm_isdst = -1;
|
|
|
|
time_t_sec: time_t;
|
|
if #complete ct.time_zone == {
|
|
case .LOCAL;
|
|
time_t_sec = timelocal(*parts);
|
|
case .UTC;
|
|
time_t_sec = timegm(*parts);
|
|
}
|
|
|
|
if time_t_sec == -1 return .{};
|
|
|
|
a_seconds := seconds_to_apollo(time_t_sec);
|
|
a_milliseconds := milliseconds_to_apollo(ct.millisecond);
|
|
result := a_seconds + a_milliseconds + APOLLO_TIME_FOR_JAN_1_1970;
|
|
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
get_apollo_moon_landing_time_on_january_1_1970_for_unix :: () -> Apollo_Time {
|
|
c1970: Calendar_Time;
|
|
c1970.year = 1970;
|
|
c1970.time_zone = .UTC;
|
|
|
|
a1970 := calendar_to_apollo(c1970);
|
|
// print("a1970: %\n", a1970);
|
|
|
|
return a1970;
|
|
}
|
|
*/
|
|
}
|
|
|
|
APOLLO_TIME_FOR_JAN_1_1970 :: Apollo_Time.{low=0xdbd87f0edb7c8000, high=0x300}; // For easy conversions from/to Unix timestamps
|
|
|
|
|
|
FEMTOSECONDS :: 1_000_000_000_000_000;
|