Skip to content
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

time: add Time.IsDST() bool method #42102

Closed
jufemaiz opened this issue Oct 21, 2020 · 8 comments
Closed

time: add Time.IsDST() bool method #42102

jufemaiz opened this issue Oct 21, 2020 · 8 comments

Comments

@jufemaiz
Copy link
Contributor

jufemaiz commented Oct 21, 2020

I propose to expose a method or capability to determine if, for a given time.Location and a given time.Time, whether the zone is a daylight savings time or not.

At this stage, arbitrary selection of two time.Time within a time.Location is required to determine if, maybe, there is a daylight savings offset being applied (see reference to golang-nuts from 2013).

Rationale

  1. isDST is already available for the zones that make up a time.Location
  2. Business requirements sometimes necessitate the ignoring of daylight savings in a particular timezone, such as for determining rules
  3. Other languages make this data available (ruby, python, Java, etc)

Proposed Options

Explicit function, with amendment of time.Location's location function

Add an IsDST method to the time.Location with a time.Time input. Return isDST for the zone applied, amending location on time.Location to also return isDST for the requested sec.

// IsDST returns a boolean flag for a given Time that indicates if the Time is in a DST zone for the Location
func(l *Location) IsDST(t Time) bool {
	sec := t.Unix()
	_, _, _, _, isDST := l.lookup(sec)
	return isDST
}

// lookup returns information about the time zone in use at an
// instant in time expressed as seconds since January 1, 1970 00:00:00 UTC.
//
// The returned information gives the name of the zone (such as "CET"),
// the start and end times bracketing sec when that zone is in effect,
// the offset in seconds east of UTC (such as -5*60*60), and whether
// the daylight savings is being observed at that time.
func (l *Location) lookup(sec int64) (name string, offset int, start, end int64, isDST bool) {
	l = l.get()

	if len(l.zone) == 0 {
		name = "UTC"
		offset = 0
		start = alpha
		end = omega
		isDST = false
		return
	}

	if zone := l.cacheZone; zone != nil && l.cacheStart <= sec && sec < l.cacheEnd {
		name = zone.name
		offset = zone.offset
		start = l.cacheStart
		end = l.cacheEnd
		isDST = zone.isDST
		return
	}

	if len(l.tx) == 0 || sec < l.tx[0].when {
		zone := &l.zone[l.lookupFirstZone()]
		name = zone.name
		offset = zone.offset
		start = alpha
		if len(l.tx) > 0 {
			end = l.tx[0].when
		} else {
			end = omega
		}
		isDST = zone.isDST
		return
	}

	// Binary search for entry with largest time <= sec.
	// Not using sort.Search to avoid dependencies.
	tx := l.tx
	end = omega
	lo := 0
	hi := len(tx)
	for hi-lo > 1 {
		m := lo + (hi-lo)/2
		lim := tx[m].when
		if sec < lim {
			end = lim
			hi = m
		} else {
			lo = m
		}
	}

	zone := &l.zone[tx[lo].index]
	name = zone.name
	offset = zone.offset
	start = tx[lo].when
	// end = maintained during the search
	isDST = zone.isDST

	// If we're at the end of the known zone transitions,
	// try the extend string.
	if lo == len(tx)-1 && l.extend != "" {
		if ename, eoffset, estart, eend, eisDST, ok := tzset(l.extend, end, sec); ok {
			return ename, eoffset, estart, eendm eisDST
		}
	}

	return
}
...
// tzset takes a timezone string like the one found in the TZ environment
// variable, the end of the last time zone transition expressed as seconds
// since January 1, 1970 00:00:00 UTC, and a time expressed the same way.
// We call this a tzset string since in C the function tzset reads TZ.
// The return values are as for lookup, plus ok which reports whether the
// parse succeeded.
func tzset(s string, initEnd, sec int64) (name string, offset int, start, end int64, isDST, ok bool) {
	var (
		stdName, dstName     string
		stdOffset, dstOffset int
	)

	stdName, s, ok = tzsetName(s)
	if ok {
		stdOffset, s, ok = tzsetOffset(s)
	}
	if !ok {
		return "", 0, 0, 0, false, false
	}

	// The numbers in the tzset string are added to local time to get UTC,
	// but our offsets are added to UTC to get local time,
	// so we negate the number we see here.
	stdOffset = -stdOffset

	if len(s) == 0 || s[0] == ',' {
		// No daylight savings time.
		return stdName, stdOffset, initEnd, omega, false, true
	}

	dstName, s, ok = tzsetName(s)
	if ok {
		if len(s) == 0 || s[0] == ',' {
			dstOffset = stdOffset + secondsPerHour
		} else {
			dstOffset, s, ok = tzsetOffset(s)
			dstOffset = -dstOffset // as with stdOffset, above
		}
	}
	if !ok {
		return "", 0, 0, 0, false, false
	}

	if len(s) == 0 {
		// Default DST rules per tzcode.
		s = ",M3.2.0,M11.1.0"
	}
	// The TZ definition does not mention ';' here but tzcode accepts it.
	if s[0] != ',' && s[0] != ';' {
		return "", 0, 0, 0, false, false
	}
	s = s[1:]

	var startRule, endRule rule
	startRule, s, ok = tzsetRule(s)
	if !ok || len(s) == 0 || s[0] != ',' {
		return "", 0, 0, 0, false, false
	}
	s = s[1:]
	endRule, s, ok = tzsetRule(s)
	if !ok || len(s) > 0 {
		return "", 0, 0, 0, false, false
	}

	year, _, _, yday := absDate(uint64(sec+unixToInternal+internalToAbsolute), false)

	ysec := int64(yday*secondsPerDay) + sec%secondsPerDay

	// Compute start of year in seconds since Unix epoch.
	d := daysSinceEpoch(year)
	abs := int64(d * secondsPerDay)
	abs += absoluteToInternal + internalToUnix

	startSec := int64(tzruleTime(year, startRule, stdOffset))
	endSec := int64(tzruleTime(year, endRule, dstOffset))
	if endSec < startSec {
		startSec, endSec = endSec, startSec
		stdName, dstName = dstName, stdName
		stdOffset, dstOffset = dstOffset, stdOffset
	}

	// The start and end values that we return are accurate
	// close to a daylight savings transition, but are otherwise
	// just the start and end of the year. That suffices for
	// the only caller that cares, which is Date.
	if ysec < startSec {
		return stdName, stdOffset, abs, startSec + abs, false, true
	} else if ysec >= endSec {
		return stdName, stdOffset, endSec + abs, abs + 365*secondsPerDay, false, true
	} else {
		return dstName, dstOffset, startSec + abs, endSec + abs, true, true
	}
}

Current Go source

References

@gopherbot
Copy link

Change https://golang.org/cl/264077 mentions this issue: time: add IsDST function on a Location for a given Time

@rsc rsc added this to Active in Proposals (old) Oct 21, 2020
@rsc rsc changed the title proposal: time: Expose isDST for a requested time.Time proposal: time: add Time.IsDST() bool method Oct 21, 2020
@jufemaiz
Copy link
Contributor Author

For those following, perhaps this "hack" can help in the meantime.

https://github.com/ace-teknologi/isdst

Cannot be assured that it'll work for all time.Times in a zone.

@rsc
Copy link
Contributor

rsc commented Oct 28, 2020

I misunderstood the proposal when I retitled it. It doesn't really make sense for the Location to be answering a question about a Time. The Time has its own Location built in. The method should be on the Time itself: t.IsDST, not t.Location().IsDST(t). I retitled it assuming the method would be on time.Time.

Otherwise, this seems fine (with the method on time.Time).
Does anyone object to this?

@jufemaiz
Copy link
Contributor Author

@rsc looking at it more I think you're right wrt method being on time.Time - my initial approach was from trawling through the codebase and working back from the Location -> Zone combination.

Happy to hear other thoughts.

@rsc
Copy link
Contributor

rsc commented Nov 4, 2020

Based on the discussion above, this seems like a likely accept.

@rsc rsc moved this from Active to Likely Accept in Proposals (old) Nov 4, 2020
@rsc
Copy link
Contributor

rsc commented Nov 11, 2020

No change in consensus, so accepted.

@rsc rsc moved this from Likely Accept to Accepted in Proposals (old) Nov 11, 2020
@rsc rsc changed the title proposal: time: add Time.IsDST() bool method time: add Time.IsDST() bool method Nov 11, 2020
@rsc rsc modified the milestones: Proposal, Backlog Nov 11, 2020
@gopherbot
Copy link

Change https://golang.org/cl/307210 mentions this issue: [release-branch.go1.16] time: add Time.IsDST() to check if its Location is in Daylight Savings Time

@rsc rsc mentioned this issue Jun 10, 2021
83 tasks
@gopherbot
Copy link

Change https://golang.org/cl/326789 mentions this issue: time: fix receiver for Time.IsDST method

gopherbot pushed a commit that referenced this issue Jun 17, 2021
Only methods that modify the time take pointer receivers;
IsDST does not modify it and therefore should not.

For #42102 and #46688.

Change-Id: I4721ef7f4d7572236ae6e4d99a459b9ffb11999e
Reviewed-on: https://go-review.googlesource.com/c/go/+/326789
Trust: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
@golang golang locked and limited conversation to collaborators Jun 10, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
No open projects
Development

Successfully merging a pull request may close this issue.

3 participants