Go: time: add Time.IsDST() bool method

Created on 21 Oct 2020  路  6Comments  路  Source: golang/go

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

Proposal Proposal-Accepted Proposal-FinalCommentPeriod

Most helpful comment

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?

All 6 comments

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

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.

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?

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

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

No change in consensus, so accepted.

Was this page helpful?
0 / 5 - 0 ratings