Подтвердить что ты не робот

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 день.

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

4b9b3361

Ответ 1

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

Если вы используете time.Since(), чтобы получить прошедшее время с time.Time, или когда вы вычисляете разницу между значениями 2 time.Time, используя метод Time.Sub(), результат a 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 say it 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.

Волшебная дата/время начала игрового времени Go: 2009-11-10 23:00:00 UTC
Это время, когда Go был впервые объявлен. Позвольте рассчитать, сколько лет 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.

Ответ 2

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

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

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

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

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

Вторая возможность - использовать функцию age с одним параметром. Результат также является интервалом, но в этом случае age вычесть из current_date (в полночь). Предположим, что сегодня 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

Ответ 3

Решение, предложенное 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

playground

Код вычисляет оставшиеся дни следующего незавершенного месяца на основе общего числа дней первого месяца (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
}

playground

Ответ 4

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

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

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

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

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

p := period.Between(t1, t2)
str := p.String()

Ответ 5

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

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
}

playground