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 · 7 comments
Closed

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

jufemaiz opened this issue Oct 21, 2020 · 7 comments

Comments

@jufemaiz
Copy link
Contributor

@jufemaiz 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

@gopherbot gopherbot commented Oct 21, 2020

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 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

@jufemaiz jufemaiz commented Oct 25, 2020

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 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

@jufemaiz jufemaiz commented Oct 28, 2020

@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 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 Nov 4, 2020
@rsc
Copy link
Contributor

@rsc rsc commented Nov 11, 2020

No change in consensus, so accepted.

@rsc rsc moved this from Likely Accept to Accepted in Proposals 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 gopherbot closed this in a9b3c4b Mar 16, 2021
@gopherbot
Copy link

@gopherbot gopherbot commented Apr 5, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Proposals
Accepted
Linked pull requests

Successfully merging a pull request may close this issue.

3 participants