time.Since() с месяцами и годами

Я пытаюсь преобразовать метку времени следующим образом:

2015-06-27T09:34:22+00:00

ко времени с момента форматирования, чтобы было сказано, например, 9 месяцев назад 1 день 2 часа 30 минут 2 секунды.

что-то такое.

Я использовал time.Parse и time.Since, чтобы добраться до этого:

6915h7m47.6901559s

Но как мне преобразовать оттуда? Что-то вроде этого я подумал:

for hours > 24 {
        days++
        hours -= 24
}

Но проблема в том, что это не будет точным для месяцев, потому что в месяцах может быть 28, 30 и 31 день.

Есть ли лучший способ достичь того, чего я хочу?


person gempir    schedule 10.04.2016    source источник
comment
Насколько точным он вам нужен? Неточные относительные временные метки сделали бы это немного проще.   -  person T0xicCode    schedule 10.04.2016
comment
Я бы предпочел, чтобы это было точно до второго   -  person gempir    schedule 10.04.2016
comment
@T0xicCode может дать мне менее точное решение? Может быть, я могу улучшить его как-то   -  person gempir    schedule 10.04.2016


Ответы (5)


Предисловие: я выпустил эту утилиту в github.com/icza/gox, см. timex.Diff().


Количество дней в месяце зависит от даты, как и количество дней в году (високосные годы).

Если вы используете time.Since() для получения времени, прошедшего с time.Time или при вычислении разницы между двумя time.Time значениями с помощью Time.Sub(), результатом будет time.Duration, который теряет контекст времени (поскольку Duration — это просто разница во времени в наносекундах). Это означает, что вы не можете точно и однозначно рассчитать разницу в годах, месяцах и т. д. по значению Duration.

Правильное решение должно вычислять разницу в контексте времени. Вы можете рассчитать разницу для каждого поля (год, месяц, день, час, минута, секунда), а затем нормализовать результат, чтобы не было отрицательных значений. Также рекомендуется поменять местами значения Time, если связь между ними не является ожидаемой.

Нормализация означает, что если значение отрицательное, добавьте максимальное значение этого поля и уменьшите значение следующего поля на 1. Например, если seconds отрицательное, добавьте к нему 60 и уменьшите minutes на 1. На что следует обратить внимание, это при нормализации разница дней (дней в месяце), должно применяться количество дней в соответствующем месяце. Это можно легко рассчитать с помощью этой маленькой хитрости:

// Max days in year y1, month M1
t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
daysInMonth := 32 - t.Day()

Логика этого заключается в том, что день 32 больше, чем максимальный день в любом месяце. Он будет автоматически нормализован (лишние дни переносятся на следующий месяц, а день уменьшается правильно). И когда мы вычтем день, который у нас есть после нормализации, из 32, мы получим именно то, каким был последний день в месяце.

Обработка часового пояса:

Вычисление разницы даст правильный результат только в том случае, если оба значения времени, которые мы передаем, находятся в одном и том же часовом поясе ( time.Location). Мы включаем проверку в нашу функцию: если это не так, мы «преобразовываем» одно из значений времени в то же место, что и другое, используя Time.In():

if a.Location() != b.Location() {
    b = b.In(a.Location())
}

Вот решение, которое вычисляет разницу в году, месяце, дне, часе, минуте, секунде:

func diff(a, b time.Time) (year, month, day, hour, min, sec int) {
    if a.Location() != b.Location() {
        b = b.In(a.Location())
    }
    if a.After(b) {
        a, b = b, a
    }
    y1, M1, d1 := a.Date()
    y2, M2, d2 := b.Date()

    h1, m1, s1 := a.Clock()
    h2, m2, s2 := b.Clock()

    year = int(y2 - y1)
    month = int(M2 - M1)
    day = int(d2 - d1)
    hour = int(h2 - h1)
    min = int(m2 - m1)
    sec = int(s2 - s1)

    // Normalize negative values
    if sec < 0 {
        sec += 60
        min--
    }
    if min < 0 {
        min += 60
        hour--
    }
    if hour < 0 {
        hour += 24
        day--
    }
    if day < 0 {
        // days in month:
        t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
        day += 32 - t.Day()
        month--
    }
    if month < 0 {
        month += 12
        year--
    }

    return
}

Некоторые тесты:

var a, b time.Time
a = time.Date(2015, 5, 1, 0, 0, 0, 0, time.UTC)
b = time.Date(2016, 6, 2, 1, 1, 1, 1, time.UTC)
fmt.Println(diff(a, b)) // Expected: 1 1 1 1 1 1

a = time.Date(2016, 1, 2, 0, 0, 0, 0, time.UTC)
b = time.Date(2016, 2, 1, 0, 0, 0, 0, time.UTC)
fmt.Println(diff(a, b)) // Expected: 0 0 30 0 0 0

a = time.Date(2016, 2, 2, 0, 0, 0, 0, time.UTC)
b = time.Date(2016, 3, 1, 0, 0, 0, 0, time.UTC)
fmt.Println(diff(a, b)) // Expected: 0 0 28 0 0 0

a = time.Date(2015, 2, 11, 0, 0, 0, 0, time.UTC)
b = time.Date(2016, 1, 12, 0, 0, 0, 0, time.UTC)
fmt.Println(diff(a, b)) // Expected: 0 11 1 0 0 0

Вывод, как и ожидалось:

1 1 1 1 1 1
0 0 30 0 0 0
0 0 28 0 0 0
0 11 1 0 0 0

Попробуйте его на Go Playground.

Чтобы рассчитать, сколько вам лет:

// Your birthday: let's say it's January 2nd, 1980, 3:30 AM
birthday := time.Date(1980, 1, 2, 3, 30, 0, 0, time.UTC)
year, month, day, hour, min, sec := diff(birthday, time.Now())

fmt.Printf("You are %d years, %d months, %d days, %d hours, %d mins and %d seconds old.",
    year, month, day, hour, min, sec)

Пример вывода:

You are 36 years, 3 months, 8 days, 11 hours, 57 mins and 41 seconds old.

Волшебная дата/время начала игровой площадки Го: 2009-11-10 23:00:00 UTC
Это время, когда впервые было объявлено о Го. Подсчитаем, сколько лет Go:

goAnnounced := time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC)
year, month, day, hour, min, sec := diff(goAnnounced, time.Now())
fmt.Printf("Go was announced "+
    "%d years, %d months, %d days, %d hours, %d mins and %d seconds ago.",
    year, month, day, hour, min, sec)

Выход:

Go was announced 6 years, 4 months, 29 days, 16 hours, 53 mins and 31 seconds ago.
person icza    schedule 10.04.2016

Решение, предложенное izca, прекрасно, но в нем отсутствует одна вещь. Если вы добавите следующий пример, вы увидите эффект:

a = time.Date(2015, 1, 11, 0, 0, 0, 0, time.UTC)
b = time.Date(2015, 3, 10, 0, 0, 0, 0, time.UTC)
fmt.Println(diff(a, b))
// Expected: 0 1 27 0 0 0
// Actual output: 0 1 30 0 0 0

детская площадка

Код вычисляет оставшиеся дни следующего неполного месяца на основе общего количества дней первого месяца (y1,M1), но его необходимо вычислять из предыдущего месяца месяца более поздней даты (y2,M2-1).

Окончательный код выглядит следующим образом:

package main

import (
    "fmt"
    "time"
)


func DaysIn(year int, month time.Month) int {
    return time.Date(year, month+1, 0, 0, 0, 0, 0, time.UTC).Day()
}

func Elapsed(from, to time.Time) (inverted bool, years, months, days, hours, minutes, seconds, nanoseconds int) {
    if from.Location() != to.Location() {
        to = to.In(to.Location())
    }

    inverted = false
    if from.After(to) {
        inverted = true
        from, to = to, from
    }

    y1, M1, d1 := from.Date()
    y2, M2, d2 := to.Date()

    h1, m1, s1 := from.Clock()
    h2, m2, s2 := to.Clock()

    ns1, ns2 := from.Nanosecond(), to.Nanosecond()

    years = y2 - y1
    months = int(M2 - M1)
    days = d2 - d1

    hours = h2 - h1
    minutes = m2 - m1
    seconds = s2 - s1
    nanoseconds = ns2 - ns1

    if nanoseconds < 0 {
        nanoseconds += 1e9
        seconds--
    }
    if seconds < 0 {
        seconds += 60
        minutes--
    }
    if minutes < 0 {
        minutes += 60
        hours--
    }
    if hours < 0 {
        hours += 24
        days--
    }
    if days < 0 {
        days += DaysIn(y2, M2-1)
        months--
    }
    if months < 0 {
        months += 12
        years--
    }
    return
}

func main() {
    var a, b time.Time
    a = time.Date(2015, 5, 1, 0, 0, 0, 0, time.UTC)
    b = time.Date(2016, 6, 2, 1, 1, 1, 1, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: false 1 1 1 1 1 1

    a = time.Date(2016, 1, 2, 0, 0, 0, 0, time.UTC)
    b = time.Date(2016, 2, 1, 0, 0, 0, 0, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: false 0 0 30 0 0 0

    a = time.Date(2016, 2, 2, 0, 0, 0, 0, time.UTC)
    b = time.Date(2016, 3, 1, 0, 0, 0, 0, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: false 0 0 28 0 0 0

    a = time.Date(2015, 2, 11, 0, 0, 0, 0, time.UTC)
    b = time.Date(2016, 1, 12, 0, 0, 0, 0, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: false 0 11 1 0 0 0

    a = time.Date(2015, 1, 11, 0, 0, 0, 0, time.UTC)
    b = time.Date(2015, 3, 10, 0, 0, 0, 0, time.UTC)
    fmt.Println(Elapsed(a, b)) // Expected: false 0 1 27 0 0 0
}

детская площадка

person robermorales    schedule 08.03.2018

Вы можете попробовать поработать с моим пакетом date, который включает в себя period для работы с периодами времени в стиле ISO (Википедия).

Тип Period поставляется с средством форматирования, которое понимает множественное число и печатает читаемые строки, такие как «9 лет, 2 месяца» и «3 часа, 4 минуты, 1 секунда», вместе с эквивалентами ISO («P9Y2M» и «PT3H4M1S»).

Периоды, конечно, сложны из-за переменной длины дней (из-за летнего времени) и месяцев (из-за григорианского календаря). Пакет period пытается помочь вам, предоставляя API, который позволяет выполнять как точные, так и неточные вычисления. Для коротких периодов (до ±3276 часов) он может точно преобразовать длительность.

duration := time.Since(...)
p, _ := period.NewOf(duration)
str := p.String()

Если вам нужны точные длительности на более длинных промежутках, вам нужно использовать функцию «Между» (которая воплощает в себе отличный ответ icza).

p := period.Between(t1, t2)
str := p.String()
person Rick-777    schedule 11.04.2016

Если вы используете PostgreSQL, вы можете легко получить результат с помощью функции age.

Предположим, у вас есть две даты a и b.

Как сказал icza, будьте осторожны, a и b должны находиться в одном часовом поясе.

Во-первых, вы можете вызвать age с двумя параметрами, в вашем случае дата a и дата b. Эта функция возвращает тип интервала, который содержит годы, месяцы, недели, дни, часы, минуты, секунды и миллисекунды.

SELECT age('2016-03-31', '2016-06-30'); -- result is: -2 mons -30 days

Вторая возможность — использовать функцию age с одним параметром. Результатом также является интервал, но в данном случае age вычитается из текущей_даты (в полночь). Предположим, что сегодня 2016/06/16:

SELECT age(timestamp '2016-06-30'); -- result is: -14 days

Обратите внимание, ключевое слово timestamp необходимо для приведения даты «2016-06-30».

Для получения более подробной информации вы можете использовать date_part или напрямую extract функцию, которая возвращает одно конкретное поле (годы, месяцы, дни...).

SELECT date_part('month', age('2016-03-31', '2016-06-30')); --result is: -2
SELECT date_part('day',   age('2016-03-31', '2016-06-30')); --result is: -30

Полный запрос:

SELECT  
    date_part('year', diff) as year
  , date_part('month', diff) as month
  , date_part('day', diff) as day
FROM (
  SELECT age(timestamp '2016-06-30') AS diff
) as qdiff;

-- result is: 
-- year month day
-- 0    0     -14

(с CTE - Common Table Expression):

WITH qdiff AS (
  SELECT age(timestamp '2016-06-30') AS diff
)
SELECT  
    date_part('year', diff) as year
  , date_part('month', diff) as month
  , date_part('day', diff) as day
FROM qdiff

-- result is: 
-- year month day
-- 0    0     -14

Документация PostgreSQL (текущая версия): https://www.postgresql.org/docs/current/static/functions-datetime.html

person alanzirek    schedule 16.06.2016
comment
Я поддерживаю это, потому что, хотя это не имеет ничего общего с golang, это хорошо объясненный подход в sql. сэр. - person sbartell; 30.08.2017

Что-то вроде этого сработает, возможно, не самое эффективное, но максимально точное:

func main() {
    a := time.Date(2015, 10, 15, 0, 0, 0, 0, time.UTC)
    b := time.Date(2016, 11, 15, 0, 0, 0, 0, time.UTC)
    fmt.Println(monthYearDiff(a, b))
}

func monthYearDiff(a, b time.Time) (years, months int) {
    m := a.Month()
    for a.Before(b) {
        a = a.Add(time.Hour * 24)
        m2 := a.Month()
        if m2 != m {
            months++
        }
        m = m2
    }
    years = months / 12
    months = months % 12
    return
}

детская площадка

person OneOfOne    schedule 10.04.2016
comment
не могу понять, почему, но время 2015-06-27T09:34:22+00:00 возвращает 1 год и 10 месяцев, что неверно. - person gempir; 10.04.2016
comment
@danielps1 моя математика была не в порядке, сейчас обновляю пост - person OneOfOne; 10.04.2016