/chron

it's time :]

Primary LanguageGoMIT LicenseMIT

chron

it's time :]

Chron is a general purpose time library that embeds time.Time and can be used as a replacement. Chron uses time.Time for calculations, so you can trust it's accuracy.

Why? There are many reasons, but the central one is that time.Time is often used as an interface. Holidays, credit card expiration dates, hourly reporting times, postgres timestamps; all these things have different time precisions, some are used as instants, while others are used as time spans.

Chron aims to wrap time.Time and provide a more specific type system that is consistent with simplicity and beauty of time.Time and time.Duration. Chron's type system breaks up the idea of time into three interfaces:

chron.Time // a specific nanosecond in time
dura.Time  // an exact or fuzzy length of time
chron.Span // a time interval with an exact start and end, like the year 2018

The implementations of these interfaces map to different time precisions. These types make it easier to reason about the times, durations, and spans represented in your program. They also provide ways to operate on times in a way that clearly shows precision. The type system is shown below:

chron.Time implementations

chron.Year ... chron.Chron are structs that embed time.Time and are truncated to a certain precision. You know that chron.Hour will always have 0 min, sec and nanoseconds. chron.Chron is the replacement for time.Time with nanosecond precision. These structs implement chron.Time which requires conversion functions to all other types.

now := chron.Now()          // type chron.Chron 2018-02-04 04:25:20.056473271 +0000 UTC
this_micro := now.AsMicro() // type chron.Micro 2018-02-04 04:25:20.056473 +0000 UTC
this_milli := now.AsMilli() // type chron.Milli 2018-02-04 04:25:20.056 +0000 UTC
...
this_month := now.AsMonth() // type chron.Month 2018-02-01 00:00:00 +0000 UTC
this_year := now.AsYear()   // type chron.Year 2018-01-01 00:00:00 +0000 UTC
time_time := now.AsTime()   // type time.Time 2018-02-04 04:25:20.056473271 +0000 UTC

Increment and Decrement functions are also required as part of the chron.Time interface. These functions handle any possible fuzzy or exact duration (dura.Time) and return a new chron.Chron

h := chron.Now().Increment(dura.NewDuration(1, 5, 32, time.Hour * 4 + time.Minute * 15 + time.Second * 30)).AsHour()
// the hour 1 year, 5 months, 32 days, 4 hour, 15 minutes, and 30 seconds from now

While Increment and Decrement handle any time duration, simple operations are better done using the many convenience methods.

now := chron.Now() // type chron.Chron 
next_hour := chron.ThisHour().AddN(1) // type chron.Hour
five_minutes_ago := now.AddMinutes(-5) // type chron.Chron
previous_second := chron.ThisSecond().AddN(-1) // type chron.Second

JSON Unmarshaling methods support 25 different formats--more can be added by appending to chron.ParseFormats. Scan and Value methods are also implemented to allow DB support.

Becuase time.Time is embedded, time package methods can be accessed directly. Before, After, and UnmarshalJSON are overwritten, but will provide the same functionality. Before and After now handle the overlapping nature of timespans, and UnmarshalJSON adds more formats besides time.RFC3339.

Time Zones

I have been burned by timezoned time data. I am of the opinion that all times belong in UTC until a human being wants to see them. I could be naive or wrong about this. Currently chron converts all times to UTC, so using the constructors will guarantee UTC internal times. If a chron user wants to create intances via chron.Chron{...}, it is their responsibility to ensure the underlying time is in UTC.

dura.Time implmentations

dura.Time is an interface that represents the same thing as a time.Duration with one key difference. dura.Time implementations can handle fuzzy durations. The concepts Month and Year don't have fixed lengths until matched with a specific instant in time.

time.Time uses Add and AddDate to deal with these differences, but there isn't currently a way to hold an pass around a duration that has months and years in it. If time libraries start handling leap seconds in a similar way, week, day, hour, minute and second will also become fuzzy.

There are currently 2 implementations of dura.Time: dura.Duration as struct that holds years, months, days, and a time.Duration, and dura.Unit an int enum that defines standard units of time.

d := dura.NewDuration(1, 3, 15, time.Hour*12)
d = d.Mult(3) // 3 years, 9 months, 45 days, 36 hours
now := chron.Now() // 2018-02-04 21:09:50.096961028 +0000 UTC
future := now.Increment(d) //2021-12-21 09:09:50.096961028 +0000 UTC

Convenience methods have overcome the original uses for dura.Unit constants, there are left here for possible use in switch statements and as the hard coded durations in chron.Span implementations.

today := chron.today()
// before 
noon := today.Increment(dura.Hour.Mult(12)).AsHour()
// new
noon := today.AddHours(12)

chron.Span implementations

chron.Span is just a time combined with a duration. Each of the chron.Time implementations also implement chron.Span. This interface allows the compairson of timespans rather than just time instants.

tomorrow := chron.Today().AddDays(1)
noon_tomorrow := tomorrow.AddHours(12)
if tomorrow.Contains(noon_tomorrow) {
    fmt.Println(:])
}
if chron.Today().Before(tomorrow) {
    fmt.Println(:])
}

Future Plans

I actually set out to write a scheduler, then I decided I needed a library that could output a stream of times base on input arguments. Then I decided to write some time conveniece stuff to make all the odd time precision and fuzzy duration issues easier to deal with. Chron will eventually become the second thing. I plan to add time series, time sequence and relative time functionality in the near future. What exists now though is solid, all changes to current code will preserve backward compatibility.

Issues

Please make issues if you have things you want to discuss or that you think need fixing. I'm all ears.