Skip to content

proposal: time: add civil time package #19700

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
jba opened this issue Mar 24, 2017 · 79 comments
Open

proposal: time: add civil time package #19700

jba opened this issue Mar 24, 2017 · 79 comments
Labels
Milestone

Comments

@jba
Copy link
Contributor

jba commented Mar 24, 2017

I propose a package with minimal implementations of the types Date, Time and DateTime, which represent times without a corresponding location.

A civil time or date does not represent a point or interval of time, but they are useful for representing events transpiring between humans. For example, your birthday begins at midnight on your birthdate regardless of where you are in the world. If you're turning 21, you can buy a drink in New York at midnight, but teleport instantaneously to San Francisco and you'll be denied, because it is 9 PM the day before.

In practice, the main motivation for these types is to represent values in database-like storage systems, like BigQuery and Spanner (and other, non-Google products).

The package currently exists at cloud.google.com/go/civil, and has been in use by the BigQuery and Spanner client libraries for a few months. For now, I'd like to move it to golang.org/x/time/civil. It is probably too esoteric to be worth including in the standard library, but if there were ever a "second-tier" set of packages that augmented the standard library, it could live there. (See #17244.)

A CL is in progress at https://go-review.googlesource.com/c/38571.

@bradfitz
Copy link
Contributor

If you're turning 18, you can buy a drink in New York at midnight, but teleport instantaneously to San Francisco and you'll be denied, because it is 9 PM the day before.

And because the drinking age in California is 21. The ballot measure never happened.

@gopherbot gopherbot added this to the Proposal milestone Mar 24, 2017
@jba
Copy link
Contributor Author

jba commented Mar 24, 2017

21 in NY too, it turns out. Edited comment.

@kevinburke
Copy link
Contributor

fun story... the drinking age is 21 in every state because of a law that withheld federal highway money unless states mandated 21 as the drinking age. https://en.wikipedia.org/wiki/National_Minimum_Drinking_Age_Act

@rsc
Copy link
Contributor

rsc commented Mar 27, 2017

On hold for #17244, which in turn is essentially blocked on understanding the long-term plan for package management. Soon.

@FlorianUekermann
Copy link
Contributor

I left a comment on the implementation in the CL. I'll mirror the parts that are not about implementation details here:

In my opinion time.Duration already implements a civil time (see #20757 for a list of issues that may be interesting).

Please clarify the difference between a civil date and just a normal date (dates don't have timezone issues to my knowledge, especially because they don't represent a moment in time).
If a civil date is just a date I don't think creating a civil package is appropriate. A simple date package or adding a Date type to the time package would be sufficient. The latter would also be useful for reusing unexported functionality from time.

There are multiple tiny "date" packages that look more or less the same (I implemented one here https://godoc.org/github.com/infobaleen/date, but there are others which are practically identical).

@jba
Copy link
Contributor Author

jba commented Sep 25, 2017

time.Duration paired with a reference time could represent a civil DateTime (not a civil Time, which is an odd beast that is nevertheless present in many SQL versions). We'd still want another type for DateTime because of the reference time. Also, the range of time.Duration is about 290 years, not nearly long enough for the range of applications we'd like to support (Sumerian calendars to Star Trek chronologies).

Since we need a civil DateTime type, and a civil Time type to support SQL, it seems reasonable to put the Date type in the same package with them. Since the time package is big enough as it is, a small, separate package makes sense.

@FlorianUekermann
Copy link
Contributor

time.Duration paired with a reference time could represent a civil DateTime. . We'd still want another type for DateTime because of the reference time.

type Civil struct{ Date time.Date; Time time.Duration }, assuming a time.Date type (#21365) is implemented. Basically just two int64s in a struct.

Also, the range of time.Duration is about 290 years, not nearly long enough for the range of applications we'd like to support (Sumerian calendars to Star Trek chronologies).

Maybe I misunderstood the goal, as I don't see the range problem you are mentioning. Are you considering other calendars where a day doesn't have 24*60*60*1e9 nanoseconds?

@bradfitz
Copy link
Contributor

Maybe I misunderstood the goal, as I don't see the range problem you are mentioning. Are you considering other calendars where a day doesn't have 246060*1e9 nanoseconds?

Other calendars are not a goal. The time package documents that:

The calendrical calculations always assume a Gregorian calendar, with no leap seconds.

@jba
Copy link
Contributor Author

jba commented Oct 3, 2017

type Civil struct{ Date time.Date; Time time.Duration }, assuming a time.Date type (#21365) is implemented. Basically just two int64s in a struct.

Where does that type live? And what about the type that just represents a time of day (civil.TIme in my package)?

Maybe I misunderstood the goal, as I don't see the range problem you are mentioning. Are you considering other calendars where a day doesn't have 246060*1e9 nanoseconds?

I didn't understand your proposal. I thought you wanted to represent a DateTime as a duration from some single reference time, like Unix represents time as an offset from 1/1/1970.

@FlorianUekermann
Copy link
Contributor

Where does that type live?

I don't have a strong opinion on that. I would suggest putting it in "exp" first and integrating it in "time" later

And what about the type that just represents a time of day (civil.TIme in my package)?

time.Duration is sufficient (look at it's metods). Maybe alias it.
If you need something to support SQL, I think that belongs in/under the sql package. The concept of civil Date and DateTime are useful in a much wider scope, that can be supported mote easily.

@lpar
Copy link

lpar commented Apr 13, 2018

I've also needed a pure Date for SQL and other things, but I implemented it by wrapping time.Time. Might that be a better implementation approach?

@vituchon
Copy link

i don't understand why is duplicate "time: add weekday helper functions #25469" (btw please see my last comment)... please don't take me wrong i only try to understand how do you think.

@gopherbot
Copy link
Contributor

Change https://golang.org/cl/38571 mentions this issue: civil: types for civil time

@jba
Copy link
Contributor Author

jba commented Feb 28, 2019

If nothing is blocking this any longer, I'd like to proceed.

I appreciate the suggestions for alternative implementations (wrapping time.Time or using offsets from a base). They would support faster comparison and addition operations at the cost of slower construction time. I propose unexporting the civil.Date and civil.Time fields and adding getter methods, to allow for future change of implementation.

@sandipbgt

This comment has been minimized.

@jba

This comment has been minimized.

@fgblomqvist
Copy link

What's the status of this? I'm a bit unfamiliar with how these CLs work, but it looks like it hasn't seen any activity for about a month. Is it just waiting for some final confirmation on its inclusion? Right now we're importing the google package, which is a huge import, just to get access to the civil one (can't get subdirectory importing to work with go modules).

@ianlancetaylor
Copy link
Member

The proposal of creating golang.org/x/time/civil is on hold until #17244 is decided. And #17244 is on hold until the modules work settles, which may happen in 1.13.

@lpar
Copy link

lpar commented Jul 22, 2019

I'd just like to put in a word for having both zoned and unzoned civil datetime types. For calendars, you want to be able to support implicit time zone, as well as explicit.

@jba
Copy link
Contributor Author

jba commented Jul 22, 2019

Isn't a zoned civil datetime just a time.Time? As the first comment says, these types

represent times without a corresponding location.

@lpar
Copy link

lpar commented Jul 22, 2019

To clarify, I'm saying that a date/time library needs to support at least all of the following:

  • 15:04:05
  • 2006-01-02
  • 2006-01-02 15:04:05
  • 2006-01-02 15:04:05 -0500

@kardianos
Copy link
Contributor

@rsc (or more generally the Proposal committee)

I would like to re-open this proposal for consideration in order to get closure on it.

Why Now?

The SQL Server Driver currently supports "https://godoc.org/cloud.google.com/go/civil" for date, time and date time types. However, "cloud.google.com/go" is a heavy module and I would prefer we do not require it, when I really just want the civil types from it. I have extracted it from that package as "github.com/golang-sql/civil" with a PR to switch to using it denisenkom/go-mssqldb#501 . However, if this proposal was accepted and https://golang.org/cl/38571 was accepted, I would want to use that.

Why use Civil types at all?

Business applications frequently use both Date, Timestamp and Timestampz (or equivalent) types. One type of bug I have encountered is where a DB Date type is used, but in certain instances time.Time will set the wrong date based on the time of day (due to timezone). This is fixable, but in newer code I've chosen to use civil.Date. This simplifies reading and understanding the code and prevents through fundamental design an entire class of bugs.

When working with Dates in a date based scheduler, converting to civil.Date simplified the code over time.Time. Equality and inequality became much simpler cognitively and slightly simpler in code.

When sending a parameter to a database instance, can be important to have proper types so they are represented correctly the the SQL:

  • "github.com/ziutek/mymysql/mysql".Date
  • "cloud.google.com/go/civil".(Date, Time, DateTime) for spanner
  • "github.com/kshvakov/clickhouse/lib/types".(Date, DateTime)
  • "github.com/mailru/go-clickhouse".Date

What do I want

Go1.13 is almost out. I would like resolution whether x/time/civil will be a package or not. If not, I will use "github.com/golang-sql/civil". If it may, then I will wait until it gets merged. Once this is sorted out, I will encurage other database drivers to support these civil types as well.


Date, DateTime, Time (of day), and Decimal types are the last types commonly supported by Databases. I'm addressing Decimal support with https://golang.org/issue/30870 .

@ianlancetaylor
Copy link
Member

Seems like we still need to sort out #17244. But I'll take that one off hold.

@yinzara
Copy link

yinzara commented Apr 23, 2020

Any updates on this? :) Would love to see this added. See above mentioned issue.

@dwlnetnl
Copy link

I have used civil package when I wanted to refer just to a date and just to a time. The DateTime struct is a nice way of combining and I see how this can be useful. This said the background on the Go 1 time API tells a compelling story for why this API is the way it is. And I believe it is the right answer for the common case and most of the time. Even with the complexities around time zones as this forces to consider the implications.

However it is nice if there is a package that would do date and time without time zones and I think this should live in x/time/civil to indicate that it could be included in the standard library one day, most likely never will though as it’s not common enough. I understand by saying this I give an opinion that isn’t robust at all considering the ‘what goes in golang.org/x’ discussion.

@dancojocaru2000
Copy link

dancojocaru2000 commented Dec 16, 2023

@rsc

I am not 100% convinced Clock is necessary at all.

I personally find it quite necessary. The usecase is representing a regular schedule, for example the timetable of a train.

Arrival Station Departure
Brașov 06:52
06:57 Pavilion CFR Brașov Triaj 06:58
07:04 Hărman 07:05

As seen in this timetable excerpt above, train R 11361 leaves Brașov every day at 06:52. When combined with a date and the Europe/Bucharest location, on the 17 of December 2023 the ISO8601 timestamp is 2023-12-17T04:58:00Z, and on the 1st of July 2024 the timestamp is 2024-07-01T03:58:00Z.

Another example is an alarm clock, which should also not be affected by timezone or DST changes.

@tgulacsi
Copy link
Contributor

For a Time as in "point in day", a time.Duration would suffice, such that a time.Now().Truncate(24time.Hour).Add(6time.Hour+58*time.Minute).

@dancojocaru2000
Copy link

dancojocaru2000 commented Dec 16, 2023

Your message is badly formatted because of not using a code block, so it's a bit harder to read.

Assuming I understood it right, however, that's actually incorrect, particularly on days when DST changes. 6 hours and 58 minutes after midnight of the 31th of March 2024 is 07:58, and 6 hours and 58 minutes after midnight of the 27th of October 2024 is 05:58. However, on both of those days, the train I mentioned departs from the 2nd station at 06:58.

package main

import (
	"fmt"
	"time"
)

func main() {
	duration := 6*time.Hour + 58*time.Minute
	location, _ := time.LoadLocation("Europe/Bucharest")
	fmt.Println(time.Date(2024, time.March, 31, 0, 0, 0, 0, location).Add(duration))
	fmt.Println(time.Date(2024, time.July, 1, 0, 0, 0, 0, location).Add(duration))
	fmt.Println(time.Date(2024, time.October, 27, 0, 0, 0, 0, location).Add(duration))
}

Using the example above, you can see that all of the printed hours are different. As such, a time.Duration is not a replacement or a workaround for having a "point in day".

@wspurgin
Copy link

wspurgin commented Apr 3, 2025

in 2025.....

Having recently started using Go far more than I had been previously, I stumbled upon this old proposal. Forgive my long-windedness, but I'd like to revisit it. I think it's a good proposal, and I think is worth some thought (even if I'm providing too much thought 😅) to really weigh if it should be included in the standard library.

I think I've groked the existing arguments in this proposal for not adopting a separate "civil" or "non-absolute" time (dates without location, times without dates, etc.) It sounds like the predominate problem is:

Go conscientiously reduced the representation of time to one (time.Time) and introducing civil time will lead Go back to split "time" APIs and risk causing internal fragmentation (e.g., which time representation should standard feature X use or return?) per @rsc

First, a gentle rebuttal

I understand that Go made a conscience decision to have only one time representation in the language, and introducing a "civil" representation would therefore seem like a step back. I also understand that the Go team is very careful about introducing new APIs and that they are not taken lightly. Introducing this type would be a significant change to the language and would require careful consideration of the implications.

That being said, I'd point out this small but important point: there are numerous examples where the same "type" (subjectively) of data is represented multiple ways in Go with their own APIs (with subtle difference even between some).

For instance, how many ways are there to represent a "number" in Go? There's int, int32, int64, big.Int, etc. and that's just integer numbers, we can find many different other types of "number" representations too (float64, big.Rat, etc.).

I'd argue each of those has a specific purpose even if they are technically representing the same "type" of data. I don't think it necessarily a desired goal that "There Can Be Only One" representation for a type of data in Go. Which one should you use? It depends on the use-case. For example, if you need to represent a number that can be very large or very small, you might want to use big.Int or big.Rat. If you need to represent a number that is always an integer, you might want to use int. If you need to represent a number that can be a decimal, you might want to use float64.

So why is this an untenable problem when considering "time" data types? I do not believe that it is. Avoiding multiple representations of the type because there's a possibility of fragmentation is not a compelling enough reason alone in my opinion. I think, rather, that becomes an important point for how any "civil" date or time is introduced.

Therefore, I think we should reframe that problem statement. Rather I think it is truly a debate around whether the introduction of these types is widely-applicable or distinct enough from existing types for Go to add into the standard library. So far, the last word on the proposal was that the Go team is not convinced that a "civil" date or time type is widely-applicable nor distinct enough for Go to introduce it.

Second, a demonstration and proposal

Applicability

While perhaps less common than the use cases designed for with time.Time, having a civil date, time, and/or datetime without time zone or a set "location" is still routinely encountered in development practice. There may be skepticism (after all what is my word worth?), but I think the numerous "civil" package implementation (like the already mentioned cloud.google.com/go/civil) and ubiquity in other languages (Java 8 pulled in Joda) shows that these use-cases are out there and maybe more widespread than what has been found by those considering this proposal from the Go team so far. Just because another language has a feature doesn't mean it's a good idea, but I'd argue that those voicing those examples are more-so trying to demonstrate that the use-cases are out there and that they are not as uncommon as the Go team has found.

For instance, @dancojocaru2000 example of a train schedule is a good use-case many of us encounter and may even use daily as commuters. Another I'll mention is Periodic Jobs (e.g., like cron jobs) where one is typically expressing an interval or relative "moment" of occurrence when a action should be taken. I think one could claim that such "location-less" time or date problems are present in very known (and hard) problems like Interval Scheduling. Capacity planning (related) often leverages this same type of "civil" date/time representation and handling.

My belief is that many of these problems deal with a simplified calculus with time data.

  1. "calendar math"
    • i.e. flipping a calendar until a specific day is reached
  2. and what I've internally referred to as "clock spinning"
    • i.e., spinning the hands of a clock until it is at a certain time

I'm also not sure how widely-applicable the last one ("clock spinning") is in isolation, but I believe (pure conjecture) the primary reason it's included in many "civil" implementation is for "actualizing" a "civil" date into an "absolute" time.

Grab your flip calendar and your multi-handed clock, put them together in a location and you have an "absolute" time instance.

Demonstration

To evaluate whether a "civil time" API is distinct enough from time.Time, let's use the train schedule example from @dancojocaru2000's comment.

We have an application designed to manage train schedules and bookings. Trains operate on clock times and a "recurrence" (e.g., daily) to destinations - the "Schedule".

For simplicity, let's say they our train operates on a "daily" Schedule at a fixed time table, but not on Christmas in the Georgian calendar. In this case, we have a "civil" time (the clock time of day a train arrives), but we also have a "holiday" which is a "civil" date but without a "civil" time.

We could represent this data in Go today with time.Time. To do so requires conventions on fake time components and locations which have to be widely present in our app and as already mentioned, easy to subtly get wrong.

Alternatively we could use something like civil package from cloud.google.com/go to represent times and dates for the train. We'll examine both implementations below.

To use @dancojocaru2000's example schedule:

Arrival Station Departure
Brașov 06:52
06:57 Pavilion CFR Brașov Triaj 06:58
07:04 Hărman 07:05

Our task is to provide the next 7 departures from a given date (exclusive) for the train's departures from Pavilion CFR Brașov Triaj. Below you'll find two implementations of the same task. One using time.Time and one using civil.DateTime. The civil.DateTime implementation produces the correct schedules. However the time.Time implementation is incorrect because, while we used typical conventions (add midnight times to plain dates, use UTC timezone throughout the codebase), we made two very easy-to-make mistakes all the same.

https://go.dev/play/p/v6RX-be2JdY

package main

import (
	"fmt"
	"time"

	"cloud.google.com/go/civil"
)

// for simplicity we're going to make these hardcoded, but recall that holidays
// vary and may be specific month+days or might be moving year+month+days. They
// don't include times though.

// Christmas 2024 - convention is to add "UTC midnight" to the date to make it
// vaild time.Time
var timeChristmas = time.Date(2024, time.December, 25, 0, 0, 0, 0, time.UTC)

// Also Christmas 2024 - civil.Date only represents the date sans a location
var civilChristmas = civil.Date{Year: 2024, Month: time.December, Day: 25}

func main() {
	location, _ := time.LoadLocation("Europe/Bucharest")

	// Time schedule with easy to make mistake
	fmt.Println("*** time.Time schedule with two easy mistakes to make when attempting to follow conventions ***")
	// By convention the application provides time.Time in UTC. e.g., pulled from a database and converted to `time.Time`
	fmt.Println("1. Christmas is accidentally included in the next 7 departures")
	fmt.Println("2. A subtle time zone bug shifted the times we show to a user")

	endOfMarchSchedule := incorrectNextSevenDepartures(time.Date(2024, time.March, 29, 0, 0, 0, 0, time.UTC))
	fmt.Println(incorrectNextSevenDepartures(time.Date(2024, time.December, 22, 0, 0, 0, 0, time.UTC)))

	for _, arrival := range endOfMarchSchedule {
		fmt.Printf("Train departs at %s\n", arrival.In(location))
	}

	fmt.Println("****************************************")

	// Civil date schedule
	fmt.Println("*** Civil date schedule ***")
	fmt.Println("1. Christmas is not included in the next 7 departures")
	fmt.Println("2. Train schedule uses the correct time zones")

	civilEndMarchSchedule := nextSevenDepartures(civil.Date{Year: 2024, Month: time.March, Day: 29})
	fmt.Println(nextSevenDepartures(civil.Date{Year: 2024, Month: time.December, Day: 22}))

	for _, arrival := range civilEndMarchSchedule {
		fmt.Printf("Train departs at %s\n", arrival.In(location))
	}

	fmt.Println("****************************************")
}

func incorrectNextSevenDepartures(start time.Time) []time.Time {
	schedule := make([]time.Time, 7)
	// First mistake, we're using the wrong time zone, we need to remember to use the departure location
	startingDay := time.Date(start.Year(), start.Month(), start.Day(), 6, 58, 0, 0, start.Location())
	schedule[0] = startingDay.AddDate(0, 0, 1)
	for i := 1; i < 7; i++ {
		next := schedule[i-1].AddDate(0, 0, 1)
		// Second mistake, the times won't match because of convention of adding a
		// time of Midnight to a plain date. To correct we would need to remember
		// to use Format to check just the Date element (or use Date to compare the
		// components directly)
		// e.g.,
		// if next.Format(time.DateOnly) == timeChristmas.Format(time.DateOnly)
		// or
		// y, m, d := next.Date()
		// if y == timeChristmas.Year() && m == timeChristmas.Month() && d == timeChristmas.Day()
		if next == timeChristmas {
			next = next.AddDate(0, 0, 1)
		}

		schedule[i] = next
	}
	return schedule
}

func nextSevenDepartures(start civil.Date) []civil.DateTime {
	schedule := make([]civil.DateTime, 7)
	// We're saved from our first mistake because there's no time zone to worry
	// over at this point
	trainTime := civil.Time{Hour: 6, Minute: 58}
	schedule[0] = civil.DateTime{
		Date: start.AddDays(1),
		Time: trainTime,
	}
	for i := 1; i < 7; i++ {
		nextDay := civil.DateTime{
			Date: schedule[i-1].Date.AddDays(1),
			Time: trainTime,
		}
		// We're saved from our second mistake because of type checking, to compare
		// to the civil.Date Christmas holiday we need to use the civil.Date type
		if nextDay.Date == civilChristmas {
			nextDay.Date = nextDay.Date.AddDays(1)
		}
		schedule[i] = nextDay
	}
	return schedule
}

While contrived, this example demonstrates that while conventions make it possible to use time.Time to represent a "civil" date and time, it's also rather easy to get wrong. Conversely, with an actual type representing a "civil" date and datetime, the language helps us side-step those easy mistakes altogether. However, to provide a location-bounded "absolute time" in the end to our user (which they ultimately need), we have to do so Just In Time (pun intended).

Proposed Full API

On the subject of names, the old joke comes to mind:

The two hardest problems in computer science are:

  1. Naming things
  2. Cache invalidation
  3. Off-by-one errors

I know @rsc recommended Day and Clock, and I'm not sure I can come up with a better set if "civil" types need to also live in time.

For the sake of clarity in this proposal of aligning on an interface, however, let's assume we use a separate civil package. Let's also assume that we'll implement all 3 types civil.Date, civil.ClockTime, and civil.Time. The last one being a combination of the first two (our flip calendar and 4-handed clock together).

For the sake of this proposal, I've put these as "interfaces" just to align on what the API would look like. I think they would be better implemented as structs with the existing time.Time under-the-hood, but I don't think it's necessary to demonstrate that API at this point.

// A civil.Date represents a single calendar day.
// Its range is January 1, 0001 to December 31, 9999.

// DateFrom returns the Date for the given year, month, and day-of-month.
//
// Example:
//
//	day := civil.DateFrom(2024, time.January, 1)
func DateFrom(year int, month time.Month, day int) Date

// DateOf returns the Date for the given time.Time.
//
// Example:
//
//	now := time.Now()
//	day := civil.DateOf(now)
func DateOf(time.Time) Date

type Date interface {
	Year() int
	Month() time.Month
	Day() int
	MonthDay() int
	YearDay() int
	Weekday() time.Weekday
	ISOWeek() (year, week int)

	Add(years int, months int, days int) Date
	Sub(e Date) (years int, months int, days int)

	After(e Date) bool
	Before(e Date) bool
	Compare(e Date) int

	Format(layout string) string
	String() string

	MarshalBinary() ([]byte, error)
	MarshalText() ([]byte, error)
	UnmarshalBinary(data []byte) error
	UnmarshalText(data []byte) error
	Scan(v any) error
	Value() (driver.Value, error)

	ParseDate(layout, value string) (Date, error)
}

// A ClockTime represents a 24-hour clock time during an unspecified day.
// Its range is 00:00:00 (midnight) to 23:59:59.999999999

// ClockTimeOf returns the ClockTime for the given hour, minute, second.
//
// Example:
//
//	now := time.Now()
//	clock := civil.ClockTimeOf(now)
func ClockTimeOf(time.Time) ClockTime

// ClockTimeFrom returns the ClockTime for the given hour, minute, second, and nanoseconds.
// Example:
//
//	clock := civil.ClockTimeFrom(12, 30, 45, 9999)
func ClockTimeFrom(hour, min, sec, nano int)

type ClockTime interface {
	Hour() int
	Minute() int
	Second() int
	Nanosecond() int

	Add(hours, mins, secs, nanos int) ClockTime
	Sub(d ClockTime) (hours, mins, secs, nanos int)

	After(d ClockTime) bool
	Before(d ClockTime) bool
	Compare(d ClockTime) int

	Format(layout string) string
	String() string

	MarshalBinary() ([]byte, error)
	MarshalText() ([]byte, error)
	UnmarshalBinary(data []byte) error
	UnmarshalText(data []byte) error
	Scan(v any) error
	Value() (driver.Value, error)

	ParseClockTime(layout, value string) (ClockTime, error)
}

// A civil.Time represents a specific civil date and time.

// civil.Of returns the civil.Time for given time.Time.
//
// Example:
//
//	now := time.Now()
//	dateTime := civil.Of(now)
func Of(time.Time) Time

// civil.From returns the civil.Time for given civil.Date and civil.Time.
// Example:
//
//	date := civil.DateFrom(2024, time.January, 1)
//	clock := civil.ClockTimeFrom(12, 30, 45, 0)
//	dateTime := civil.From(date, clock)
func From(date Date, clock ClockTime) Time

type Time interface {
	Date() Date
	ClockTime() ClockTime

	After(e Time) bool
	Before(e Time) bool
	Compare(e Time) int

	In(loc *time.Location) time.Time

	Format(layout string) string
	String() string

	MarshalBinary() ([]byte, error)
	MarshalText() ([]byte, error)
	UnmarshalBinary(data []byte) error
	UnmarshalText(data []byte) error
	Scan(v any) error
	Value() (driver.Value, error)

	ParseTime(layout, value string) (Time, error)
}

First, I think it's important to note what can and cannot be done with these interfaces.

One can:

  • Go easily from an "absolute" time.Time to a "civil" date, a clock time, or the combined civil.Time representation.
  • Create a "civil" date or clock time from individual components (e.g., year, month, day, hour, minute, second).
  • Perform "calendar math" on the civil.Date type and "spin" the civil.ClockTime
  • Marshal/unmarshal the civil.Date, civil.ClockTime, and civil.Time types to/from binary and text formats.
  • sql.Scanner and driver.Valuer interfaces for the civil.Date, civil.ClockTime, and civil.Time types.

One cannot:

  • Go from a civil.Time to a time.Time without an explicit location (e.g., civil.Time.In(location)).
  • Go from a civil.ClockTime nor a civil.Date to a time.Time in isolation without going through the civil.From method - forcing a developer to be explicit about a "calendar" and a "clock" if trying to move towards an "absolute" time.
  • Perform "math" on a civil.Time type. You have to operate on the civil.Date (flip the calendar) and civil.ClockTime (spin the hands of a clock) separately.

If we're particularly worried about fragmentation, we could drop the various *From methods so that you have to start with a time.Time. This would make the API a little bit less useful, but it would mean that anyone using the API has a time.Time in hand already, so why not just use/return the time.Time? My personal opinion is that removing the *From methods are not worth it especially if there's a separate package, but I hold that opinion loosely.

Considerations

Looking at those interfaces, if I turn my head and squint, it looks very similar to time.Time. I don't think that's a bad thing. It makes it feel very idiomatic. I also believe that all of those interfaces (or really structs) could be implemented with the existing time.Time under-the-hood. That prompts the question, if we can implement this whole thing with time.Time, why not just use time.Time?

To put that question another way, what's the real distinction between that proposed full API and time.Time? There are only 4 real differences:

  1. Comparison expectations
  2. Formatting and parsing expectations
  3. civil.ClockTime "math" - i.e., spinning the hands of the clock
  4. civil.Time.In does not represent the same "absolute" time instance in a new location

Comparison

For this I mean, that civil.Date and civil.ClockTime are compared by their respective components (e.g., year, month, day and hour, minute, second) and not a comparison of a specific "absolute" time instance. This seems like the most obvious distinction from time.Time, and I don't think it is an overly controversial point.

Formatting

For this I mean, which "layout(s)" is considered a "correct" layout when parsing or formatting for a civil.Date, civil.ClockTime, or civil.Time, and ultimately the default format used in String().

As an example what would you expect civil.DateFrom(2024, time.January, 1).String() to return? I think most people would expect time.DateOnly - 2024-01-01 and not 2024-01-01 00:00:00 +0000 UTC. What about civil.ClockTimeFrom(12, 30, 45, 0).String()? I think most people would expect time.TimeOnly - 12:30:45 and not 1970-01-01 12:30:45 +0000 UTC. The same goes for the civil.Time type. I think most people would expect time.DateTime - 2024-01-01 12:30:45 and not 2024-01-01 12:30:45 +0000 UTC.

ClockTime "math"

For this I mean, spinning the hands of a clock is fundamentally different than actual time math (time.Time.Add) because of what happens when you "roll-over" past the 00:00:00 mark on a clock (in either direction).

For instance, if you are at 23:00:00 on our 4-handed clock, and spin the nanosecond hand forward 3.6e+12 "ticks" (1 hour), you would expect to end up at the 00:00:00 mark with no conception of a "day" having been added. However, if you were to do the same thing with time.Time, you would be adding a day to the time.

Jumping locations

For this I mean, that civil.Time.In "jumps" the calendar and clock to the new location. For instance, if you have a civil.Time of 2024-01-01 12:30:45 and you call civilTime.In(chicago), it will return a time.Time of 2024-01-01 12:30:45 -0600 CST. However, if you have a time.Time of 2024-01-01 12:30:45 +0000 UTC and you call In(chicago) on it, it will return a time.Time of 2024-01-01 06:30:45 -0600 CST.

An alternative name to civil.Time.In to instill that distinction might be civil.Time.Jump

Less "civil" API Proposal

Of those distinction, if we reject the default formatting and parsing expectations (after all you can use time.Time.Format and time.Parse for these today), as well as the need or applicability of ClockTime "math", we're left with only two real distinction: Comparisons and "Jumping" locations

If we were to remove the civil.ClockTime type, remove Format, String(), Parse* we'd be left with something that is strikingly similar to time.Time. In that case, I think it would be better to just use time.Time and not introduce a new type. However, we could still introduce Jump and comparison helper functions (e.g., AfterDate, BeforeDate, CompareDate, EqualDate) to satisfy the "civil" date cases and help avoid the "easy to make mistakes" we saw in the example above.

package time

type Time struct {
  // contains filtered or unexported fields
}

//....
// Jump returns a new Time with the same date, and time components but in the given location.
func (t Time) Jump(loc *Location) Time

// AfterDate reports whether t is after the given time by date (regardless of time).
func (t Time) AfterDate(u Time) bool

// BeforeDate reports whether t is before the given time by date (regardless of time).
func (t Time) BeforeDate(u Time) bool

// CompareDate compares t and u by date (regardless of time).
func (t Time) CompareDate(u Time) int

// EqualDate reports whether t and u are equal by date (regardless of time).
func (t Time) EqualDate(u Time) bool

This is a much smaller changeset to the language. A time.Time.Jump function feels like a useful addition to the time.Time API. I think that the AfterDate, BeforeDate, CompareDate, and EqualDate functions would also be useful additions. They feel aligned to a precedence with time.Time.AddDate already existing. I personally feel like remembering to use these comparison functions versus remembering to use time.Time.Format() is more idiomatic at least. Moreover, to @rsc's point about fragmentation, this doesn't produce any "split" in the API. There's still only one representation for time in Go: time.Time.

Our corrected example would then look something like:

package main

import (
	"fmt"
	"time"
)

var timeChristmas = time.Date(2024, time.December, 25, 0, 0, 0, 0, time.UTC)

func main() {
	endOfMarchSchedule := nextSevenDepartures(time.Date(2024, time.March, 29, 0, 0, 0, 0, time.UTC))
	fmt.Println(nextSevenDepartures(time.Date(2024, time.December, 22, 0, 0, 0, 0, time.UTC)))

	location, _ := time.LoadLocation("Europe/Bucharest")
	for _, arrival := range endOfMarchSchedule {
		fmt.Printf("Train departs at %s\n", arrival.Jump(location))
	}
}

func nextSevenDepartures(start time.Time) []time.Time {
	schedule := make([]time.Time, 7)
	startingDay := time.Date(start.Year(), start.Month(), start.Day(), 6, 58, 0, 0, time.UTC)
	schedule[0] = startingDay.AddDate(0, 0, 1)
	for i := 1; i < 7; i++ {
		next := schedule[i-1].AddDate(0, 0, 1)
		if next.EqualDate(timeChristmas) {
			next = next.AddDate(0, 0, 1)
		}

		schedule[i] = next
	}
	return schedule
}

While one has to carry around time components on things that don't really have them, I don't feel like this is a huge burden. You do have to be consistent with your time zones if doing any time math, but as that's already the convention, I feel like these additions just make it easier to avoid subtle mistakes.

Conclusion

I think this proposal is worth revisiting.

It's worth debating whether the outlined "civil" date and time types in a standlone civil package is distinct enough from time.Time to warrant its inclusion in the standard library. I believe it could be, but only if included in its entirety. If we reject certain distinctions' merits or applicability, we may be better off just adding a Jump method to time.Time and perhaps a few date-only comparison methods. Either way, I hope this serves as a good starting point for discussion.

Lastly, I want to thank the Go team for their hard work and dedication to the language. I hope this proposal is taken up again for consideration and that we can have a fruitful discussion about it.

@neild
Copy link
Contributor

neild commented Apr 3, 2025

I think we have a general agreement on what the shape of an API would look like. @rsc's Day/Clock (#19700 (comment)), cloud.google.com/go/civil, and @wspurgin's proposal above are all fairly similar. I think the main unresolved question is whether this would be new types in the time package, or a new time/civil package.

The question, as I see it, is whether we should add this at all.

Does this need to be in std? Is a third-party package like cloud.google.com/go/civil sufficient?

In general, we add new API to std when it is

  1. difficult to implement outside of the standard library;
  2. widely useful; or
  3. having a single implementation enables interoperability.

For example, TB.Parallel is difficult to implement outside the testing package, TB.TempDir is trivial to implement but widely useful, and the recently-accepted TB.Attr enables interoperability between different systems that produce or consume test attributes.

Civil time is not difficult to implement outside the standard library.

I don't think we have much evidence for against how widely useful it is. pkg.go.dev lists 499 importers of cloud.google.com/go/civil, but I think many of those are forked versions of cloud.google.com packages and don't really constitute use of the package. Perhaps a survey of real usage of existing civil time implementations (including non-Go ones like ABSL's civil time) would constitute evidence for or against adopting something in std.

There's a clear argument from interoperability, but it turns on usage: If programmers often pass civil times between modules, then this is evidence that a common type definition would be useful. If few programs need civil times, or if those times are not passed between modules, then this may not be a strong argument.

Speaking as someone who is not on the proposal committee and has no deciding power here, I think the best way to move this proposal forward is to gather evidence that there's a need for it in std. (A train scheduler might need a civil time type, but does it need one defined in the standard library?)

@wspurgin
Copy link

wspurgin commented Apr 9, 2025

Thanks for the feedback @neild. I appreciate the time you took to respond. These are all good and valid points, however I want to challenge a few of your concerns (though I recognize that you aren't on the proposal committee).

Perhaps a survey of real usage of existing civil time implementations (including non-Go ones like ABSL's civil time) would constitute evidence for or against adopting something in std.

I think that would be valuable, but in terms of practically obtainable evidence, what would constitute "proof" of "real usage" of other civil time implementations?

For example, I could list a bunch of references in literature in which various problems sets interact with a "location-less" date/time (i.e., the scheduling class of problems). I could also point out its prevalence at very large software companies (e.g., google made CCTZ and cloud.google.com/go/civil to use it "for real" 😄). I could point at other standard library adoptions, Joda started out as its own "civil" time library (and more) that was so widely useful and popular that Java 8 adopted it into the standard. Java 8 and its later variants account for 89% of the Java ecosystem (as of 2023). Is that proof of real usage or the need for interoperability?

I (subjectively) view all of these as proof for how widely useful civil time implementations are.

On the subject of pkg.go.dev, in brief, I agree that it does not have any meaningful statistics we could use to understand the usage of go specific civil implementations (like Google's aforementioned one). Unlike the npm registry we don't have a sense of how many downloads a package has through pkg.go.dev. In many cases where I've needed a "civil" date/time representation, those are applications that would download a package like this, but we wouldn't be publishing our resulting application or library to pkg.go.dev or otherwise making it publicly available to the index (which is all the "Imported By" numbers can track)... I imagine that much of the "real usage" would fall into a similar situation.

Barring a hat-in-hand straw survey asking Gopher's to respond with a "use it" or "don't use it", I don't know how we could get a sense of the "real usage" of civil time implementations (or attempts at it) in Go specifically. Which is beyond my abilities to gather, and in my opinion unnecessary to prove the utility of civil time in Go.

Civil time is not difficult to implement outside the standard library.... A train scheduler might need a civil time type, but does it need one defined in the standard library?

Perhaps, but to your early point TB.TempDir was also trivial. In my opinion, a civil date/time implementation being trivial to implement is not an argument against its inclusion in the standard library especially as I think it's clear that such a trivial addition is widely useful.

A standard civil time implementation leveraging time.Time centralizes the conventions tied-to how one uses time.Time to represent a "civil" date/time. That, to me, is a net positive for the Go ecosystem.

@apparentlymart
Copy link

apparentlymart commented Apr 9, 2025

To me (also not on the proposal committee!), the primary question from @neild's analysis is the matter of whether multiple independently-maintained codebases are likely to benefit from having a shared agreement on one idiomatic way to represent each of the concepts under discussion.

If we focus on that specific question then perhaps it can be answered by looking for currently-existing (not hypothetical) examples of situations such as:

  • Two independently-maintained codebases both import the same third-party library for representations of these concepts and pass values of these types between their public API boundaries.
  • Two independently-maintained codebases started with their own inline representations of these concepts but then later needed to integrate and had to implement some glue code to adapt from one representation to another, or to use some sort of workaround like agreeing on a specific set of conventions by which to (ab)use time.Time for the cross-codebase interactions.
  • (This one's a little harder to nail down) Situations where someone declined to solve a problem in Go at all because it would've resulted in one of the previous two problems. For example, perhaps they chose to write their software in a different language that had better support for these concepts in its standard library.

Although of course there can be other reasons to justify including something in the standard library, the need for multiple codebases to agree on a representation or approach often seems to be a strong justification, and so if we can find examples of that then I'd hope that would be compelling enough to make the other avenues of argument less important.

@cespare
Copy link
Contributor

cespare commented Apr 9, 2025

I think that a civil date is widely useful. (But I have never used a civil time.)

At work we have our own implementation that's similar to civil.Date but with a few tweaks. We use it a fair amount.

One unfortunate thing that happens without a civil date representation is that there are many scenarios where a time.Time can be used as a poor substitute for a date type, but a civil date would be more correct. So it's hard to tell how widely used a common civil date type ought to be used if you are only looking for, for instance, imports of cloud.google.com/go/civil.

And in fact, if I'm writing code outside of my work monorepo, I would probably be inclined to make do without a date type if I'm only doing a few simple date calculations, just to avoid a dependency.

So I'm strongly in favor of this proposal, or at least a limited version that only adds Date.

Finally, I will also mention that I don't love depending on the cloud.google.com module. I would be more comfortable with using it if it were in golang.org/x/time instead.

@jba
Copy link
Contributor Author

jba commented Apr 10, 2025

@cespare can you describe the tweaks you made? What is omitted from Russ's time.Day proposal or cloud.google.com/go/civil.Date that you need?

I agree that having a unified API is worth it here, and the problem with cloud.google.com/go/civil is that it drags in dependencies many people don't want.

Personally I think that this package should live in golang.org/x/time/civil, as I said in the top post, but I know Russ would rather pull some of those x packages into the standard library; there's no difference between the stdlib and the x repos (except x/exp) in terms of support or proposal process, so why not have everything in one place?

@wspurgin
Copy link

I'm not the subject of your question @jba, but for me, Russ's omitted:

  • Day() int to get the day date component (maybe just by accident)
  • Compare() int for <=> on the civil date
  • sql Scan & Value
  • a means to get that civil date (called Day in Russ' proposal) from a time.Time without first calling time.Time.Date()

The last of which is very minor and if undesired I wouldn't be too broken up about.

On the subject of packages, IMO (having no say or even as great of experience with Go as many here), I interpreted the x repos as x == extra and while they may have experimental or "looser" compatibility (according to the wiki), in practice it doesn't seem like they have been any looser (except for x/exp as you said).

If that's the case, a x/time/civil feels like a great way to not clutter the time stdlib package with what is certainly a less frequently used (if still widely useful) adjacent package to its primary use. Especially

@cespare
Copy link
Contributor

cespare commented Apr 10, 2025

@jba

@cespare can you describe the tweaks you made? What is omitted from Russ's time.Day proposal or cloud.google.com/go/civil.Date that you need?

Ours is much closer to civil.Date; Russ's time.Day is missing some stuff that we would definitely use like In (a way to go from Date/Day to a time.Time).

Comparing our internal Date to civil.Date, there are a few minor differences which don't matter much or which I would do differently now. Ignoring those, there are a couple of methods we have which civil.Date doesn't have:

func TodayUTC() Date
    TodayUTC returns the current date in UTC.

func (d Date) TruncateMonth() Date
    TruncateMonth returns the date of the first day of the month of d.

func (d Date) TruncateYear() Date
    TruncateYear returns the date of the first day of the year of d.

The other difference is that our In behaves differently from civil.Date.In when it comes to the edge cases:

func (d Date) In(loc *time.Location) time.Time
    In returns the earliest time in the day d at the location.

    This is usually 00:00:00, but might be some later time if there was
    no 00:00:00 time on that day in the given location. For example,
    Cuba observes daylight savings time shifts that skip midnight, so if loc is
    "America/Havana", then we get

        Date{Year: 2019, Month: time.March, Day: 10}.In(loc) // 2019-03-10 01:00:00 -0400 CDT

    while time.Date gives

        time.Date(2019, time.March, 10, 0, 0, 0, 0, loc) // 2019-03-09 23:00:00 -0500 CST

    In panics if loc is nil. In also panics if called with a Date that does not
    exist (such as Date{2000, 1, 42}).

That said, we chose to diverge because we knew that we didn't want to bring in the civil.Date dependency in the first place. I think I'd be happy bringing in a golang.org/x dependency to replace our custom Date and working around any API issues on our end.

@neild
Copy link
Contributor

neild commented Apr 10, 2025

I (subjectively) view all of these as proof for how widely useful civil time implementations are.

I think this is all evidence that civil time implementations are common.

Evidence that they're useful would be code that uses those existing implementations. If civil time is widely useful, there should be ample code out there which uses it. (In the case of implementations that include both absolute and civil times--to use the ABSL terminology--this would be code that specifically uses the civil time component.)

Unrelated, on terminology: Joda time appears to call civil time "local time", with LocalDate, LocalTime, and LocalDateTime classes.

@dylan-bourque
Copy link

dylan-bourque commented Apr 10, 2025

Adding my $0.02 ...

I've repeatedly come across problems that have required both civil time and civil date throughout my career, initially in the early 00s working in C++. I found that code useful enough that I ported it to C# and later to Go here. I called the types date.Value and timeofday.Value, but they are semantically equivalent. I created that project very early in my Go career so it's certainly not my best work, but it was valuable enough that I felt the need to carry it along with me in my move from C# to Go. Interestingly, the justifications and explanations in the docs for that project, which I originally created 20 years ago and and ported to Go in 2019, pretty much mirror what people have been pointing out here.

As a point of consideration, cloud.google.com/go, which contains the civil package, has 451 dependencies at v0.120.0. That's A LOT of additional code to bring into a project in order to have representations of a calendar date and wall clock. That module having over 100 releases also raises flags for me, since it would be a source of code churn in any consuming project. 😬

Having worked at multiple companies that were extremely security conscious, an external dependency of that size would raise all kinds of flags where this being part of Go's "standard library" would not. At each of those companies I would have been required to create an internal module rather than take on a dependency on cloud.google.com/go.

If civil time is widely useful, there should be ample code out there which uses it.

A big caveat here is that it disregards non-public code. In every case where I've needed this functionality, it wasn't part of an OSS project. Please don't limit the evaluation of utility to only publicly available code. This is not the first time that I've been part of Go discussions that, intentionally or not, assumed that non-OSS code doesn't exist.

As a pedantic side note, if I were building this for myself today I would probably consider calling Date Calendar instead, since the discussions of "day" and "date" are really references to a day on the Gregorian calendar. I've also thought about using Clock or ClockTime instead of Time for similar reasons, but I'm less convinced of that one.

@jeffreydwalter

This comment has been minimized.

@dylan-bourque

This comment has been minimized.

@wspurgin
Copy link

wspurgin commented Apr 10, 2025

To pull the conversation back to a civil spot

I (subjectively) view all of these as proof for how widely useful civil time implementations are.

I think this is all evidence that civil time implementations are common.

Evidence that they're useful would be code that uses those existing implementations. If civil time is widely useful, there should be ample code out there which uses it. (In the case of implementations that include both absolute and civil times--to use the ABSL terminology--this would be code that specifically uses the civil time component.)

@neild - I understand your perspective, but I disagree. There is ample code using this (there are multiple folks in this thread to attest to their own anecdotal evidence of that) - it's just not easily publicly available. However, if you are only satisfied by publicly available numbers, then may I point out the js-joda npm registery link I posted in my earlier comment #19700 (comment)? ~1.5M weekly downloads points to real usage of just a single civil time implementation. Even if you discount downloads from npm (for reasons), it's instructive on how useful it is.

--- edit --- to add more examples with public numbers for other implementations

--- end edit ---

By comparison, a very popular and widely used framework like React has ~40M weekly downloads.

Unrelated, on terminology: Joda time appears to call civil time "local time", with LocalDate, LocalTime, and LocalDateTime classes.

Yes, however since go time.Location is already used with the time.Time implementation, I'd worry that using similar language to Joda as LocalDate / LocalTime / LocalDateTime would confuse developers as the roots (Local / Location) are so similar. What we're proposing is a "Location-less" representation of date/time in Go.

Also "Local Time" in RFC3339 defines "local time" as the local system at a given local UTC offset. "Unqualified Local Time" is what it refers to as what ABSL calls civil time. 🤷 naming is hard. golang.org/x/time/civil.Date is likely the best fully qualified name that produces the least confusion.

@seankhliao
Copy link
Member

I don't think use in other languages is good evidence, different languages are used for different purposes, we want to see how this addition would fit in to the Go ecosystem.

Note that downloads provide inflated numbers and should not be considered an accurate representation of popularity. It's why we generally prefer instances of use in code, and by proxy, package imports in Go.

@jeffreydwalter
Copy link

@seankhliao who is "we"?

@apparentlymart
Copy link

apparentlymart commented Apr 11, 2025

For what it's worth, I wasn't intending #19700 (comment) to be a trick question or for it to be a barrier to this proposal being accepted. Quite the contrary actually: I expected that it wouldn't be hard at all to find examples of library APIs that need to include values corresponding to the concepts we've been discussing in this issue, but have ended up employing weird workarounds to deal with the fact that there's no single idiomatic representation of these concepts in the ecosystem.


Here's one example which I found by using GitHub Code Search to find references to some different representations of "civil date" and then use some human intuition to try to spot cases which seemed like they might be compensating in some way for a lack of a common vocabulary type to talk about dates in the Go ecosystem.

The following example is from a codebase called "Sybil". I have no affiliation whatsoever with this codebase and don't mean any kind of endorsement of it; it's just an example I found in my research. The repository says it's under the MIT license and so I'm sharing the following snippets of it in good faith under the terms of that license.

Inside this codebase there is an interface called DividendRequester which has the following method:

	DividendRequest(ctx context.Context, ticker string, start, end time.Time) ([]*ent.DividendCreate, []time.Time, error)

This takes two time.Time values. The caller of this method sets start based on a time.Time field called ListDate from struct Entity, which is already a little suspicious since the field name implies that it's a date but its type implies that it's an absolute instant in time.

There are currently two implementations of DividendRequest that each seem to wrap some API for querying dividends. The following are relevant snippets showing what they are each doing with those start and end arguments:

  • func (am *Alpaca) DividendRequest(ctx context.Context, ticker string, start, end time.Time) ([]*ent.DividendCreate, []time.Time, error) {
    	request := marketdata.GetCorporateActionsRequest{
    		Symbols: []string{ticker},
    		Types:   []string{"cash_dividend"},
    		Start:   civil.DateOf(start),
    		End:     civil.DateOf(end),
    	}
    	// ...

    This one is working with the upstream package github.com/alpacahq/alpaca-trade-api-go/v3/marketdata, whose authors apparently decided to use cloud.google.com/go/civil.Date directly as part of their public API, and so the implementation converts the two time.Time values while silently discarding their time and location information.

  • func (pio *Polygonio) DividendRequest(ctx context.Context, ticker string, start, end time.Time) (results []*ent.DividendCreate, payDates []time.Time, err error) {
    	// ...
    	startDate := models.Date(start)
    	endDate := models.Date(end)
    
    	params := models.ListDividendsParams{
    		TickerEQ:   &ticker,
    		PayDateGTE: &startDate,
    		PayDateLT:  &endDate,
    	}.WithOrder(models.Asc).WithLimit(1000)

    This one is instead working with the upstream package github.com/polygon-io/client-go/rest/models, which has its own "date" type. It turns out that this particular "date" is actually just another time.Time but with MarshalJSON and UnmarshalJSON overridden to produce a YYYY-MM-DD string instead of a full timestamp string, and so again this is silently discarding the time and location information.

Some general observations, then:

  • There are at least three different representations of the idea of a "date" (without an associated time or location) used by this codebase.

  • Two of them are overpromising by actually representing an absolute instant but then discarding everything but the date part.

  • One is just a date, but that comes at the expense of importing the entire Google Cloud client library for Go even though this overall program doesn't seem to interact directly with Google Cloud Platform at all (as far as I can tell).

  • Since I have no relationship with the authors of this codebase I don't have any knowledge of their intentions, but it sure seems like this DividendRequest API is intending to take two date values, and is only using time.Time because there is no common idiomatic representation of just a date that both of the DividendRequest implementations can agree on.

    (Although I didn't show all the details of it here since this comment was already very long, I note that DividendRequest also returns time.Time values, which I can see from the implementation are naked dates being artificially interpreted as instants in the "local" timezone: alpaca implementation, polygonio implementation.)

I expect that having a Date type in some package of the standard library would've helped all three of the codebases involved in this implementation to agree on a common representation of "date": it would be explicit in the DividendRequest API that this is working only with a date and that there's no meaningful time/location component to either start or end in this API, and the different implementations of this interface would not all need to include distracting adapter code to convert from one non-standardized representation to another.

Since these three codebases were all seemingly developed by disjoint sets of authors, a third-party library offering a date type could only solve this problem if all three groups had independently chosen the same third-party.

@wspurgin
Copy link

wspurgin commented Apr 11, 2025

@seankhliao thank you for weighing in. Like others here, I would like to gently challenge your position:

Downloads as a metric

Note that downloads provide inflated numbers and should not be considered an accurate representation of popularity. It's why we generally prefer instances of use in code, and by proxy, package imports in Go.

I'm certainly not claiming that downloads are a perfect metric. They have inflation (CI/CD, bots, etc.) and deflation (i.e., caching - once a version is downloaded, package managers won't download it again unless forced). My comment #19700 (comment) includes a link if you're curious to learn more.

Despite that, that's why I referenced a irrefutably popular package like React in that comment as a comparison. For whatever imperfection downloads has as a metric, it includes uses in applications and libraries alike both of open and closed source. The numbers themselves in isolation aren't as meaningful, but comparing the metric to something known to be popular and widely useful still has meaning. They share the imperfections (systemic error / bias), and the correlation, therefore, can still be proof of the trend of popularity regardless. Don't take my word for it, that's a known statistical fact about correlation.

Package Imports in Go

Package imports in Go as a metric is also imperfect (and perhaps more).

  • Package imports are also inflated - forks on GitHub artificially drive this number up.
  • Package imports do not include use-cases where a package is used in non-indexable application
  • Package imports will never include closed source

It also is not a good barometer for whether something is widely applicable enough to live in a stdlib or not. The x/image/riff is imported by a paltry 74 packages - yet it is in the standard library. Is it less widely applicable therefore and undeserving of its home in the stdlib? No.

Ignoring other languages

I don't think use in other languages is good evidence, different languages are used for different purposes, we want to see how this addition would fit in to the Go ecosystem.

This is a reduction. Go is a general purpose language - like many others. It has differences in design and philosophy, yes, but it is not so singular as to abhor applicability of a use-case though it be from a separate language.

Moreover, this argument is in danger of an Appeal To Ignorance fallacy. We can still learn from other languages faults and successes as evidence. Rejecting this as evidence of the use-case simply because "it's not Go" is not compelling.

For instance, I particularly picked the Javascript ecosystem because, like Go, there is only one representation of time in the standard library for Javascript - its (poorly named) Date class. In fact, it is a very similar implementation to time.Time (an epoch offset numeric).

The Javascript community there has attempted to fill the gap this proposal describes with those 3rd-party packages I listed in that earlier comment. Use the Dependants metric if you reject downloads - tens of thousands of other packages leverage those various different interpretations of the "civil" date/time. This produces a large interoperability challenge in the language.

As an example, look at a package trying to provide a framework for giving users a Datepicker: https://www.npmjs.com/package/@mui/x-date-pickers - it has to support many of these popular libraries (Luxon, Day.js, date-fns, Moment.js) rather than a shared singular one the language could have provided.


Numbers as requested

To provide a summary of the numbers you prefer (bearing in mind the imperfections of this metric I described above):

I picked these three by searching date and civil in pkg.go.dev. As @apparentlymart clear and thoughtful effort shows #19700 (comment) there's many places where a package does not exist, but a civil "date"/"time" like construct does. I do not see a practical means of obtaining that information in a statistically satisfiable means beyond a survey of Go developers (as I mentioned before to @neild). Which is an untenable ask to make of a random developer like myself 😄

A resolution as requested

Finally, 8 years is a long time for this proposal to sit in purgatory. As this repo's readme states, the goal is:

Make sure that proposals get a proper, fair, timely, recorded evaluation with a clear answer.

I think, whatever the outcome, the committee should take up this proposal to reach a long-deserved resolution on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Incoming
Development

No branches or pull requests