diff --git a/datetime/compare.go b/datetime/compare.go new file mode 100644 index 0000000..1656898 --- /dev/null +++ b/datetime/compare.go @@ -0,0 +1,78 @@ +package datetime + +import ( + "time" + + "golang.org/x/tools/go/analysis/passes/bools" +) + +// IsZero retourne vrai si la date représente le 01/01/0001 à 00:00 UTC. +func IsZero(t time.Time) bool { + return t.IsZero() +} + +// Compare retourne : +// - 1 si t1 est dans le futur de t2, +// - 0 si t1 et t2 sont égaux, +// - -1 sinon. +func Compare(t1, t2 time.Time) int { + u1, u2 := t1.Unix(), t2.Unix() + + switch { + case u1 == u2: + return 0 + case u1 > u2: + return 1 + default: + return -1 + } +} + +// Eq retourne vrai si t1 = t2. +func Eq(t1, t2 time.Time) bool { return Compare(t1, t2) == 0 } + +// Ne retourne vrai si t1 ≠ t2. +func Ne(t1, t2 time.Time) bool { return Compare(t1, t2) != 0 } + +// Gt retourne vrai si t1 > t2. +func Gt(t1, t2 time.Time) bool { return Compare(t1, t2) > 0 } + +// Ge retourne vrai si t1 ≥ t2. +func Ge(t1, t2 time.Time) bool { return Compare(t1, t2) >= 0 } + +// Lt retourne vrai si t1 < t2. +func Lt(t1, t2 time.Time) bool { return Compare(t1, t2) < 0 } + +// Le retourne vrai si t1 ≤ t2. +func Le(t1, t2 time.Time) bool { return Compare(t1, t2) <= 0 } + +// Min retourne la date la plus située dans le passé. +func Min(t time.Time, times ...time.Time) time.Time { + for _, tt := range times { + if Lt(tt, t) { + t = tt + } + } + + return t +} + +// Max retourne la date la plus située dans le futur. +func Max(t time.Time, times ...time.Time) time.Time { + for _, tt := range times { + if Gt(tt, t) { + t = tt + } + } + + return t +} + +// IsNow retourne vrai si la date est actuelle. +func IsNow(t time.Time) bool { return Eq(t, time.Now()) } + +// IsPast retourne vrai si la date est située dans le passé. +func IsPast(t time.Time) bool { return Lt(t, time.Now()) } + +// IsFuture retourne vrai si la date est située dans le future. +func IsFuture(t time.Time) bool { return Gt(t, time.Now()) } diff --git a/datetime/const.go b/datetime/const.go new file mode 100644 index 0000000..aeac6ae --- /dev/null +++ b/datetime/const.go @@ -0,0 +1,257 @@ +package datetime + +import ( + "time" +) + +var ( + // Fuseau horaire par défaut + DefaultTZ = time.Local + + // Jours de la semaine (format court) + shortDays = []string{ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + } + + // Jours de la semaine (format long) + longDays = []string{ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + } + + // Mois (format court) + shortMonths = []string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + } + + // Mois (format long) + longMonths = []string{ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + } + + formatters = map[rune]func(time.Time) string{ + // Jour + 'j': format_j, // Jour du mois sans les 0 initiaux (1-31) + 'd': format_d, // Jour du mois, sur deux chiffres (avec un 0 initial) (01-31) + 'N': format_N, // Représentation numérique ISO 8601 du jour de la semaine (1-7) + 'w': format_w, // Jour de la semaine au format numérique (0-6) + 'z': format_z, // Jour de l’année (0-365) + 'D': format_D, // Jour de la semaine, en 3 lettres (Mon-Sun) + 'l': format_l, // Jour de la semaine, textuel, version longue (Monday-Sunday) + + // Semaine + 'W': format_W, // N° de semaine dans l’année ISO 8601, les semaines commencent le lundi (0-53) + + // Mois + 'n': format_n, // Mois sans les 0 initiaux (1-12) + 'm': format_m, // Mois au format numérique, avec 0 initiaux (01-12) + 't': format_t, // Nombre de jours dans le mois (28-31) + 'M': format_M, // Mois, en trois lettres (Jan-Dec) + 'F': format_F, // Mois, textuel, version longue (January-December) + + // Année + 'y': format_y, // Année sur 2 chiffres (Exemples : 99 ou 03) + 'Y': format_Y, // Année sur au moins 4 chiffres, avec - pour les années av. J.-C. (Exemples : -0055, 0787, 1999, 2003, 10191) + 'L': format_L, // Est ce que l’année est bissextile (1 si bissextile, 0 sinon) + + // Heure + 'a': format_a, // Ante meridiem et Post meridiem en minuscules (am ou pm) + 'A': format_A, // Ante meridiem et Post meridiem en majuscules (AM ou PM) + 'B': format_B, // Heure Internet Swatch (000-999) + 'g': format_g, // Heure, au format 12h, sans les 0 initiaux (1-12) + 'G': format_G, // Heure, au format 24h, sans les 0 initiaux (0-23) + 'h': format_h, // Heure, au format 12h, avec les 0 initiaux (01-12) + 'H': format_H, // Heure, au format 24h, avec les 0 initiaux (00-23) + 'i': format_i, // Minutes avec les 0 initiaux (00-59) + 's': format_s, // Secondes avec les 0 initiaux (00-59) + 'v': format_v, // Nillisecondes avec les 0 initiaux (000-999) + 'u': format_u, // Microsecondes avec les 0 initiaux (000000-999999) + + // Fuseau horaire + 'T': format_T, // Abréviation du fuseau horaire, si connu ; sinon décalage depuis GMT (Exemples : EST, MDT, +05) + 'e': format_e, // L’identifiant du fuseau horaire (Exemples : UTC, GMT, Atlantic/Azores) + 'I': format_I, // L’heure d’été est activée ou pas (1 si oui, 0 sinon) + 'O': format_O, // Différence d’heures avec l’heure de Greenwich (GMT), sans deux-points entre les heures et les minutes (Exemple : +0200) + 'P': format_P, // Différence d’heures avec l’heure de Greenwich (GMT), avec deux-points entre les heures et les minutes (Exemple : +02:00) + 'p': format_p, // Identique à P, mais retourne Z au lieu de +00:00 + 'Z': format_Z, // Décalage horaire en secondes. Le décalage des zones à l’ouest de la zone UTC est négatif, et à l’est, il est positif.(-43200 à 50400) + + // Date et heure complète + 'c': format_c, // Date au format ISO 8601 (2004-02-12T15:19:21+00:00) + 'r': format_r, // Date au format RFC 5322 (Thu, 21 Dec 2000 16:01:070200) + 'U': format_U, // Secondes depuis l’époque Unix (1er Janvier 1970, 0h00 00s GMT) + } + + parsers = map[rune]string{ + // Jours + 'j': "2", // Jour du mois sans les 0 initiaux (1-31) + 'd': "02", // Jour du mois, sur deux chiffres (avec un 0 initial) (01-31) + 'D': "Mon", // Jour de la semaine, en 3 lettres (Mon-Sun) + 'l': "Monday", // Jour de la semaine, textuel, version longue (Monday-Sunday) + + // Mois + 'n': "1", // Mois sans les 0 initiaux (1-12) + 'm': "01", // Mois au format numérique, avec 0 initiaux (01-12) + 'M': "Jan", // Mois, en trois lettres (Jan-Dec) + 'F': "January", // Mois, textuel, version longue (January-December) + + // Année + 'y': "06", // Année sur 2 chiffres (Exemples : 99 ou 03) + 'Y': "2006", // Année sur au moins 4 chiffres, avec - pour les années av. J.-C. (Exemples : -0055, 0787, 1999, 2003, 10191) + + // Heure + 'a': "pm", // Ante meridiem et Post meridiem en minuscules (am ou pm) + 'A': "PM", // Ante meridiem et Post meridiem en majuscules (AM ou PM) + 'g': "3", // Heure, au format 12h, sans les 0 initiaux (1-12) + 'h': "03", // Heure, au format 12h, avec les 0 initiaux (01-12) + 'H': "15", // Heure, au format 24h, avec les 0 initiaux (00-23) + 'i': "04", // Minutes avec les 0 initiaux (00-59) + 's': "05", // Secondes avec les 0 initiaux (00-59) + + // Fuseau horaire + 'T': "MST", // Abréviation du fuseau horaire, si connu ; sinon décalage depuis GMT (Exemples : EST, MDT, +05) + 'O': "-0700", // Différence d’heures avec l’heure de Greenwich (GMT), sans deux-points entre les heures et les minutes (Exemple : +0200) + 'P': "-07:00", // Différence d’heures avec l’heure de Greenwich (GMT), avec deux-points entre les heures et les minutes (Exemple : +02:00) + + // Date et heure complète + 'c': "2006-01-02T15:04:05-07:00", // Date au format ISO 8601 (2004-02-12T15:19:21+00:00) + 'r': "Thu, 21 Dec 2000 16:01:07 +0200", // Date au format RFC 5322 (Thu, 21 Dec 2000 16:01:070200) + } + + layouts = []string{ + DayDateTimeLayout, + DateTimeLayout, DateTimeNanoLayout, ShortDateTimeLayout, ShortDateTimeNanoLayout, + DateLayout, DateNanoLayout, ShortDateLayout, ShortDateNanoLayout, + ISO8601Layout, ISO8601NanoLayout, + RFC822Layout, RFC822ZLayout, RFC850Layout, RFC1123Layout, RFC1123ZLayout, RFC3339Layout, RFC3339NanoLayout, RFC1036Layout, RFC7231Layout, + KitchenLayout, + CookieLayout, + ANSICLayout, + UnixDateLayout, + RubyDateLayout, + "2006", + "2006-1", "2006-1-2", "2006-1-2 15", "2006-1-2 15:4", "2006-1-2 15:4:5", "2006-1-2 15:4:5.999999999", + "2006.1", "2006.1.2", "2006.1.2 15", "2006.1.2 15:4", "2006.1.2 15:4:5", "2006.1.2 15:4:5.999999999", + "2006/1", "2006/1/2", "2006/1/2 15", "2006/1/2 15:4", "2006/1/2 15:4:5", "2006/1/2 15:4:5.999999999", + "2006-01-02 15:04:05PM MST", "2006-01-02 15:04:05.999999999PM MST", "2006-1-2 15:4:5PM MST", "2006-1-2 15:4:5.999999999PM MST", + "2006-01-02 15:04:05 PM MST", "2006-01-02 15:04:05.999999999 PM MST", "2006-1-2 15:4:5 PM MST", "2006-1-2 15:4:5.999999999 PM MST", + "1/2/2006", "1/2/2006 15", "1/2/2006 15:4", "1/2/2006 15:4:5", "1/2/2006 15:4:5.999999999", + "2006-1-2 15:4:5 -0700 MST", "2006-1-2 15:4:5.999999999 -0700 MST", + "2006-1-2T15:4:5Z07", "2006-1-2T15:4:5.999999999Z07", + "2006-1-2T15:4:5Z07:00", "2006-1-2T15:4:5.999999999Z07:00", + "2006-1-2T15:4:5-07:00", "2006-1-2T15:4:5.999999999-07:00", + "20060102150405-07:00", "20060102150405.999999999-07:00", + "20060102150405Z07", "20060102150405.999999999Z07", + "20060102150405Z07:00", "20060102150405.999999999Z07:00", + } +) + +const ( + // Unités de durée + Nanosecond = time.Nanosecond + Microsecond = time.Microsecond + Millisecond = time.Millisecond + Second = time.Second + Minute = time.Minute + Hour = time.Hour + Day = time.Hour * 24 + Week = Day * 7 + + MonthsPerYear = 12 + DaysPerWeek = 7 + HoursPerDay = 24 + HoursPerWeek = HoursPerDay * DaysPerWeek + MinutesPerHour = 60 + MinutesPerDay = MinutesPerHour * HoursPerDay + MinutesPerWeek = MinutesPerHour * HoursPerWeek + SecondsPerMinute = 60 + SecondsPerHour = SecondsPerMinute * MinutesPerHour + SecondsPerDay = SecondsPerMinute * MinutesPerDay + SecondsPerWeek = SecondsPerMinute * MinutesPerWeek + + // Erreurs + errInvalidTZ = "Invalid timezone %q" + errInvalidValue = "Cannot parse string %q as datetime, please make sure the value is valid" + + // Quelques layouts + ANSICLayout = time.ANSIC + UnixDateLayout = time.UnixDate + RubyDateLayout = time.RubyDate + RFC822Layout = time.RFC822 + RFC822ZLayout = time.RFC822Z + RFC850Layout = time.RFC850 + RFC1123Layout = time.RFC1123 + RFC1123ZLayout = time.RFC1123Z + RssLayout = time.RFC1123Z + KitchenLayout = time.Kitchen + RFC2822Layout = time.RFC1123Z + CookieLayout = "Monday, 02-Jan-2006 15:04:05 MST" + RFC3339Layout = "2006-01-02T15:04:05Z07:00" + RFC3339MilliLayout = "2006-01-02T15:04:05.999Z07:00" + RFC3339MicroLayout = "2006-01-02T15:04:05.999999Z07:00" + RFC3339NanoLayout = "2006-01-02T15:04:05.999999999Z07:00" + ISO8601Layout = "2006-01-02T15:04:05-07:00" + ISO8601MilliLayout = "2006-01-02T15:04:05.999-07:00" + ISO8601MicroLayout = "2006-01-02T15:04:05.999999-07:00" + ISO8601NanoLayout = "2006-01-02T15:04:05.999999999-07:00" + RFC1036Layout = "Mon, 02 Jan 06 15:04:05 -0700" + RFC7231Layout = "Mon, 02 Jan 2006 15:04:05 MST" + DayDateTimeLayout = "Mon, Jan 2, 2006 3:04 PM" + DateTimeLayout = "2006-01-02 15:04:05" + DateTimeMilliLayout = "2006-01-02 15:04:05.999" + DateTimeMicroLayout = "2006-01-02 15:04:05.999999" + DateTimeNanoLayout = "2006-01-02 15:04:05.999999999" + ShortDateTimeLayout = "20060102150405" + ShortDateTimeMilliLayout = "20060102150405.999" + ShortDateTimeMicroLayout = "20060102150405.999999" + ShortDateTimeNanoLayout = "20060102150405.999999999" + DateLayout = "2006-01-02" + DateMilliLayout = "2006-01-02.999" + DateMicroLayout = "2006-01-02.999999" + DateNanoLayout = "2006-01-02.999999999" + ShortDateLayout = "20060102" + ShortDateMilliLayout = "20060102.999" + ShortDateMicroLayout = "20060102.999999" + ShortDateNanoLayout = "20060102.999999999" + TimeLayout = "15:04:05" + TimeMilliLayout = "15:04:05.999" + TimeMicroLayout = "15:04:05.999999" + TimeNanoLayout = "15:04:05.999999999" + ShortTimeLayout = "150405" + ShortTimeMilliLayout = "150405.999" + ShortTimeMicroLayout = "150405.999999" + ShortTimeNanoLayout = "150405.999999999" +) diff --git a/datetime/create.go b/datetime/create.go new file mode 100644 index 0000000..036d42d --- /dev/null +++ b/datetime/create.go @@ -0,0 +1,101 @@ +package datetime + +import ( + "fmt" + "time" + + . "gitea.zaclys.com/bvaudour/gob/option" +) + +// Zero retourne la date correspondant au 01/01/0001 00:00:00 UTC. +// Si tz est fourni, la date est dans le fuseau horaire demandé, +// sinon, dans le fuseau horaire par défaut. +func Zero(tz ...string) Result[time.Time] { + var t time.Time + + return toTZ(t, tz...) +} + +// Now retourne la date actuelle. +func Now(tz ...string) Result[time.Time] { + t := time.Now() + + return toTZ(t, tz...) +} + +// Tomorrow retourne la date dans 24h. +func Tomorrow(tz ...string) Result[time.Time] { + now := Now(tz...) + if t, ok := now.Ok(); ok { + return Ok(AddDays(t, 1)) + } + + return now +} + +// Yesterday retourne la date il y a 24h. +func Yesterday(tz ...string) Result[time.Time] { + now := Now(tz...) + if t, ok := now.Ok(); ok { + return Ok(AddDays(t, -1)) + } + + return now +} + +// FromTimestamp convertit un timestamp (exprimé en secondes) en date. +func FromTimestamp(timestamp int64, tz ...string) Result[time.Time] { + t := time.Unix(timestamp, 0) + + return toTZ(t, tz...) +} + +// FromTimestampMilli convertit un timestamp (exprimé en ms) en date. +func FromTimestampMilli(timestamp int64, tz ...string) Result[time.Time] { + t := time.Unix(timestamp/1e3, (timestamp%1e3)*1e6) + + return toTZ(t, tz...) +} + +// FromTimestampMicro convertit un timestamp (exprimé en μs) en date. +func FromTimestampMicro(timestamp int64, tz ...string) Result[time.Time] { + t := time.Unix(timestamp/1e6, (timestamp%1e6)*1e3) + + return toTZ(t, tz...) +} + +// FromTimestampNano convertit un timestamp (exprimé en ns) en date. +func FromTimestampNano(timestamp int64, tz ...string) Result[time.Time] { + t := time.Unix(timestamp/1e9, timestamp%1e9) + + return toTZ(t, tz...) +} + +// FromDateTime retourne la date à partir des données numériques complètes. +func FromDateTime(year, month, day, hour, minute, second int, tz ...string) Result[time.Time] { + location, ok := getTZ(tz...).Get() + if !ok { + return Err[time.Time](fmt.Errorf(errInvalidTZ, tz)) + } + + t := time.Date(year, time.Month(month-1), day, hour, minute, second, 0, location) + + return Ok(t) +} + +// FromDate retourne le début du jour à partir des données de dates. +func FromDate(year, month, day int, tz ...string) Result[time.Time] { + return FromDateTime(year, month, day, 0, 0, 0) +} + +// FromTime retourne la date d’aujourd’hui à l’heure indiquée. +func FromTime(hour, minute, second int, tz ...string) Result[time.Time] { + now := Now(tz...) + if !now.IsOk() { + return now + } + + n, _ := now.Ok() + year, month, day := n.Date() + return FromDateTime(year, int(month+1), day, hour, minute, second, tz...) +} diff --git a/datetime/duration.go b/datetime/duration.go new file mode 100644 index 0000000..365bc8f --- /dev/null +++ b/datetime/duration.go @@ -0,0 +1,72 @@ +package datetime + +import ( + "math" + "time" +) + +// DiffInYears retourne (t1 - t2) exprimé en années. +func DiffInYears(t1, t2 time.Time) int64 { + y1, m1, d1 := t1.Date() + y2, m2, d2 := t2.Date() + + dy, dm, dd := y1-y2, m1-m2, d1-d2 + if dm < 0 || (dm == 0 && dd < 0) { + dy-- + } + if dy < 0 && (dd != 0 || dm != 0) { + dy++ + } + + return int64(dy) +} + +// DiffInMonths retourne (t1 - t2) exprimé en mois. +func DiffInMonths(t1, t2 time.Time) int64 { + y1, m1, d1 := t1.Date() + y2, m2, d2 := t2.Date() + + dy, dm, dd := y1-y2, m1-m2, d1-d2 + if dd < 0 { + dm-- + } + if dy == 0 && dm == 0 { + return int64(dm) + } + if dy == 0 && dm != 0 && dd != 0 { + dh := abs(DiffInHours(t1, t2)) + if int(dh) < DaysInMonth(t1)*HoursPerDay { + return int64(0) + } + return int64(dm) + } + + return int64(dy*MonthsPerYear + int(dm)) +} + +// DiffInWeeks retourne (t1 - t2) exprimé en semaines. +func DiffInWeeks(t1, t2 time.Time) int64 { + return int64(math.Floor(float64(DiffInSeconds(t1, t2)) / float64(SecondsPerWeek))) +} + +// DiffInDays retourne (t1 - t2) exprimé en jours. +func DiffInDays(t1, t2 time.Time) int64 { + return int64(math.Floor(float64(DiffInSeconds(t1, t2)) / float64(SecondsPerDay))) +} + +// DiffInHours retourne (t1 - t2) exprimé en heures. +func DiffInHours(t1, t2 time.Time) int64 { + + return DiffInSeconds(t1, t2) / SecondsPerHour +} + +// DiffInMinutes retourne (t1 - t2) exprimé en minutes. +func DiffInMinutes(t1, t2 time.Time) int64 { + + return DiffInSeconds(t1, t2) / SecondsPerMinute +} + +// DiffInSeconds retourne (t1 - t2) exprimé en secondes. +func DiffInSeconds(t1, t2 time.Time) int64 { + return t1.Unix() - t2.Unix() +} diff --git a/datetime/format.go b/datetime/format.go new file mode 100644 index 0000000..db9b307 --- /dev/null +++ b/datetime/format.go @@ -0,0 +1,206 @@ +package datetime + +import ( + "fmt" + "strings" + "time" +) + +func abs[N ~int | ~int64](e N) N { + if e < 0 { + return -e + } + return e +} + +func isYearBissextil(year int) bool { return year%4 == 0 && !(year%100 == 0 && year%400 != 0) } + +func daysInMonth(year int, month time.Month) int { + switch month { + case time.February: + if isYearBissextil(year) { + return 29 + } + return 28 + case time.April, time.June, time.September, time.November: + return 30 + default: + return 31 + } +} + +func internetHour(t time.Time) int { + t, _ = ToTimezone(t, "CET").Ok() + h, m, s := t.Clock() + n := t.Nanosecond() + d := time.Duration(n)*Nanosecond + time.Duration(s)*Second + time.Duration(m)*Minute + time.Duration(h)*Hour + i := d * 1000 / Day + return int(i) +} + +func gmtDiff(t time.Time) time.Duration { + d0 := t.Day() + h0, m0, _ := t.Clock() + g, _ := ToTimezone(t, "GMT").Ok() + d1 := g.Day() + h1, m1, _ := t.Clock() + + return time.Duration(d0-d1)*Day + time.Duration(h0-h1)*Hour + time.Duration(m0-m1)*Minute +} + +// Jour +func format_j(t time.Time) string { return fmt.Sprintf("%d", t.Day()) } +func format_d(t time.Time) string { return fmt.Sprintf("%02d", t.Day()) } +func format_N(t time.Time) string { return fmt.Sprintf("%d", t.Weekday()+1) } +func format_w(t time.Time) string { return fmt.Sprintf("%d", t.Weekday()) } +func format_D(t time.Time) string { return shortDays[t.Weekday()] } +func format_l(t time.Time) string { return longDays[t.Weekday()] } +func format_z(t time.Time) string { return fmt.Sprintf("%d", t.YearDay()) } + +// Semaine +func format_W(t time.Time) string { + _, w := t.ISOWeek() + return fmt.Sprintf("%d", w) +} + +// Mois +func format_n(t time.Time) string { return fmt.Sprintf("%d", t.Month()+1) } +func format_m(t time.Time) string { return fmt.Sprintf("%02d", t.Month()+1) } +func format_M(t time.Time) string { return shortMonths[t.Month()] } +func format_F(t time.Time) string { return longMonths[t.Month()] } +func format_t(t time.Time) string { return fmt.Sprintf("%d", daysInMonth(t.Year(), t.Month())) } + +// Année +func format_y(t time.Time) string { return fmt.Sprintf("%02d", t.Year()%100) } +func format_Y(t time.Time) string { return fmt.Sprintf("%d", t.Year()) } +func format_L(t time.Time) string { + if isYearBissextil(t.Year()) { + return "1" + } + return "0" +} + +// Heure +func format_a(t time.Time) string { + if t.Hour() < 12 { + return "am" + } + return "pm" +} +func format_A(t time.Time) string { + if t.Hour() < 12 { + return "AM" + } + return "PM" +} +func format_B(t time.Time) string { return fmt.Sprintf("%03d", internetHour(t)) } +func format_g(t time.Time) string { + h := t.Hour() % 12 + if h == 0 { + h = 12 + } + return fmt.Sprintf("%d", h) +} +func format_G(t time.Time) string { return fmt.Sprintf("%d", t.Hour()) } +func format_h(t time.Time) string { + h := t.Hour() % 12 + if h == 0 { + h = 12 + } + return fmt.Sprintf("%02d", h) +} +func format_H(t time.Time) string { return fmt.Sprintf("%02d", t.Hour()) } +func format_i(t time.Time) string { return fmt.Sprintf("%02d", t.Minute()) } +func format_s(t time.Time) string { return fmt.Sprintf("%02d", t.Second()) } +func format_v(t time.Time) string { return fmt.Sprintf("%03d", t.Nanosecond()/1000000) } +func format_u(t time.Time) string { return fmt.Sprintf("%06d", t.Nanosecond()/1000) } + +// Fuseau horaire +func format_T(t time.Time) string { + name := t.Location().String() + if !strings.Contains(name, "/") { + return name + } + diff := gmtDiff(t) + h, m := diff/Hour, (diff%Hour)/Minute + s := "+" + if h < 0 { + s = "-" + } + if m == 0 { + return fmt.Sprintf("%s%02d", s, abs(h)) + } + return fmt.Sprintf("%s%02d:%02d", s, abs(h), abs(m)) +} +func format_e(t time.Time) string { return fmt.Sprintf("%s", t.Location()) } +func format_I(t time.Time) string { + if t.IsDST() { + return "1" + } + return "0" +} +func format_O(t time.Time) string { + diff := gmtDiff(t) + h, m := diff/Hour, (diff%Hour)/Minute + s := "+" + if h < 0 { + s = "-" + } + return fmt.Sprintf("%s%02d%02d", s, abs(h), abs(m)) +} +func format_P(t time.Time) string { + diff := gmtDiff(t) + h, m := diff/Hour, (diff%Hour)/Minute + s := "+" + if h < 0 { + s = "-" + } + return fmt.Sprintf("%s%02d:%02d", s, abs(h), abs(m)) +} +func format_p(t time.Time) string { + diff := gmtDiff(t) + if diff == 0 { + return "Z" + } + return format_P(t) +} +func format_Z(t time.Time) string { + diff := gmtDiff(t) + return fmt.Sprintf("%d", diff/Second) +} + +// Date et heure complète +func format_c(t time.Time) string { + // Équivalent à "Y-m-d\TH:i:sP" + return fmt.Sprintf("%s-%s-%sT%s:%s:%s%s", format_Y(t), format_m(t), format_d(t), format_H(t), format_i(t), format_s(t), format_P(t)) +} +func format_r(t time.Time) string { + // Équivalent à "D, j M Y H:i:sO" + return fmt.Sprintf("%s, %s %s %s %s:%s:%s%s", format_D(t), format_j(t), format_M(t), format_Y(t), format_H(t), format_i(t), format_s(t), format_P(t)) +} +func format_U(t time.Time) string { return fmt.Sprintf("%d", t.Unix()) } + +// Format retourne la représentation de la date dans le format indiqué. +// Pour connaître toutes les options possibles pour le format, veullez consulter +// https://www.php.net/manual/fr/datetime.format.php +// (ou le fichier const.go). +func Format(t time.Time, format string) string { + var buffer strings.Builder + + runes := []rune(format) + for i := 0; i < len(runes); i++ { + if f, ok := formatters[runes[i]]; ok { + buffer.WriteString(f(t)) + } else { + switch runes[i] { + case '\\': // raw output, no parse + buffer.WriteRune(runes[i+1]) + i++ + default: + buffer.WriteRune(runes[i]) + } + } + } + + return buffer.String() +} diff --git a/datetime/informations.go b/datetime/informations.go new file mode 100644 index 0000000..335678b --- /dev/null +++ b/datetime/informations.go @@ -0,0 +1,80 @@ +package datetime + +import ( + "time" +) + +// DaysInMonth retourne le nombre de jours dans le mois. +func DaysInMonth(t time.Time) int { return daysInMonth(t.Year(), t.Month()) } + +// DaysInYear retourne le nombe de jours dans l’année. +func DaysInYear(t time.Time) int { + if IsBissextil(t) { + return 366 + } + return 365 +} + +// IsBissextil retourne vrai si la date est située dans une année bissextile. +func IsBissextil(t time.Time) bool { return isYearBissextil(t.Year()) } + +// IsWeekend retourne vrai si la date est située dans le weekend. +func IsWeekend(t time.Time) bool { + d := t.Weekday() + + return d != time.Saturday && d != time.Sunday +} + +// IsBeginOfMonth retourne vrai si la date est dans le premier jour du mois. +func IsBeginOfMonth(t time.Time) bool { return t.Day() == 1 } + +// IsEndOfMonth retourne vrai si la date est dans le dernier jour du mois. +func IsEndOfMonth(t time.Time) bool { return t.Day() == DaysInMonth(t) } + +// IsBeginOfYear retourne vrai si la date est dans le premier jour de l’année. +func IsBeginOfYear(t time.Time) bool { return IsBeginOfMonth(t) && t.Month() == time.January } + +// IsEndOfYear retourne vrai si la date est dans le dernier jour de l’année. +func IsEndOfYear(t time.Time) bool { return IsEndOfMonth(t) && t.Month() == time.December } + +// IsToday retourne vrai si la date est située aujourd’hui. +func IsToday(t time.Time) bool { + y, m, d := t.Date() + y0, m0, d0 := time.Now().In(t.Location()).Date() + + return y == y0 && m == m0 && d == d0 +} + +// IsTomorrow retourne vrai si la date est située demain. +func IsTomorrow(t time.Time) bool { + n := time.Now().In(t.Location()) + y, m, d := t.Date() + y0, m0, d0 := n.Date() + + if d > 1 { + return y == y0 && m == m0 && d == d0+1 + } + + if m > time.January { + return y == y0 && m == m0+1 && IsEndOfMonth(n) + } + + return y == y0+1 && IsEndOfYear(n) +} + +// IsYesterday retourne vrai si la date est située hier. +func IsYesterday(t time.Time) bool { + n := time.Now().In(t.Location()) + y, m, d := t.Date() + y0, m0, d0 := n.Date() + + if d < DaysInMonth(t) { + return y == y0 && m == m0 && d == d0-1 + } + + if m < time.December { + return y == y0 && m == m0-1 && IsBeginOfMonth(n) + } + + return y == y0-1 && IsBeginOfYear(n) +} diff --git a/datetime/operations.go b/datetime/operations.go new file mode 100644 index 0000000..d481cd7 --- /dev/null +++ b/datetime/operations.go @@ -0,0 +1,266 @@ +package datetime + +import ( + "fmt" + "time" + + . "gitea.zaclys.com/bvaudour/gob/option" +) + +func addUnit[I int | int64](t time.Time, duration I, unit time.Duration) time.Time { + return t.Add(time.Duration(duration) * unit) +} + +// AddNanoseconds ajoute d ns à la date. +func AddNanoseconds[I int | int64](t time.Time, d I) time.Time { + return addUnit(t, d, Nanosecond) +} + +// AddMicroseconds ajoute d μs à la date. +func AddMicroseconds[I int | int64](t time.Time, d I) time.Time { + return addUnit(t, d, Microsecond) +} + +// AddMilliseconds ajoute d ms à la date. +func AddMilliseconds[I int | int64](t time.Time, d I) time.Time { + return addUnit(t, d, Millisecond) +} + +// AddSeconds ajoute d s à la date. +func AddSeconds[I int | int64](t time.Time, d I) time.Time { + return addUnit(t, d, Second) +} + +// AddMinutes ajoute d min à la date. +func AddMinutes[I int | int64](t time.Time, d I) time.Time { + return addUnit(t, d, Minute) +} + +// AddHours ajoute d heures à la date. +func AddHours[I int | int64](t time.Time, d I) time.Time { + return addUnit(t, d, Hour) +} + +// AddDays ajoute d jours à la date. +func AddDays[I int | int64](t time.Time, d I) time.Time { + return addUnit(t, d, Day) +} + +// AddWeeks ajoute d semaines à la date. +func AddWeeks[I int | int64](t time.Time, d I) time.Time { + return addUnit(t, d, Week) +} + +// AddMonths ajoute d mois à la date. +func AddMonths[I int | int64](t time.Time, d I) time.Time { + return t.AddDate(0, int(d), 0) +} + +// AddYears ajoute d années à la date. +func AddYears[I int | int64](t time.Time, d I) time.Time { + return t.AddDate(int(d), 0, 0) +} + +// AddDecades ajoute d×10 années à la date. +func AddDecades[I int | int64](t time.Time, d I) time.Time { + return AddYears(t, 10*d) +} + +// AddCenturies ajoute d siècles à la date. +func AddCenturies[I int | int64](t time.Time, d I) time.Time { + return AddYears(t, 100*d) +} + +func getTZ(tz ...string) (result Option[*time.Location]) { + if len(tz) > 0 { + location, err := time.LoadLocation(tz[0]) + if err == nil { + return Some(location) + } + return + } + + return Some(DefaultTZ) +} + +func toTZ(t time.Time, tz ...string) Result[time.Time] { + if l, ok := getTZ(tz...).Get(); ok { + return Ok(t.In(l)) + } + + if len(tz) == 0 { + return Ok(t.In(DefaultTZ)) + } + + return Err[time.Time](fmt.Errorf(errInvalidTZ, tz)) +} + +// ToTimezone retourne la date dans le fuseau horaire indiqué. +func ToTimezone(t time.Time, tz string) Result[time.Time] { + return toTZ(t, tz) +} + +// Local retourne la date dans le fuseau horaire local. +func Local(t time.Time) time.Time { + return t.In(time.Local) +} + +// UTC retourne la date dans le fuseau UTC. +func UTC(t time.Time) time.Time { + return t.In(time.UTC) +} + +// Date modifie la date avec l’année, le mois et le jour fournis. +func Date(t time.Time, year, month, day int) time.Time { + h, m, s := t.Clock() + e := t.Nanosecond() + l := t.Location() + + return time.Date(year, time.Month(month-1), day, h, m, s, e, l) +} + +// Clock modifie l’heure de la date. +func Clock(t time.Time, hour, minute, second int, nano ...int) time.Time { + y, m, d := t.Date() + l := t.Location() + e := 0 + + if len(nano) > 0 { + e = nano[0] + } + + return time.Date(y, m, d, hour, minute, second, e, l) +} + +// SetNano modifie les nanosecondes de la date. +func SetNano(t time.Time, nano int) time.Time { + h, m, s := t.Clock() + + return Clock(t, h, m, s, nano) +} + +// SetSecond modifie la seconde de la date (et éventuellement la ns). +func SetSecond(t time.Time, second int, nano ...int) time.Time { + h, m := t.Hour(), t.Minute() + e := t.Nanosecond() + if len(nano) > 0 { + e = nano[0] + } + + return Clock(t, h, m, second, e) +} + +// SetMinute modifie la minute de la date (et éventuellement la s et la ns). +func SetMinute(t time.Time, minute int, other ...int) time.Time { + h, s, e := t.Hour(), t.Second(), t.Nanosecond() + if len(other) > 0 { + s = other[0] + if len(other) > 1 { + e = other[1] + } + } + + return Clock(t, h, minute, s, e) +} + +// SetHour modifie l’heure de la date (et éventuellement la min, s & ns). +func SetHour(t time.Time, hour int, other ...int) time.Time { + m, s, e := t.Minute(), t.Second(), t.Nanosecond() + if len(other) > 0 { + m = other[0] + if len(other) > 1 { + s = other[1] + if len(other) > 2 { + e = other[2] + } + } + } + + return Clock(t, hour, m, s, e) +} + +// SetDay modifie le jour de la date. +func SetDay(t time.Time, day int) time.Time { + y, m := t.Year(), t.Month() + + return Date(t, y, int(m+1), day) +} + +// SetMonth modifie le mois de la date (et éventuellement le jour). +func SetMonth(t time.Time, month int, day ...int) time.Time { + y, d := t.Year(), t.Day() + if len(day) > 0 { + d = day[0] + } + + return Date(t, y, month, d) +} + +// SetYear modifie l’année de la date (et éventuellement le mois et le jour). +func SetYear(t time.Time, year int, other ...int) time.Time { + m, d := int(t.Month()+1), t.Day() + if len(other) > 0 { + m = other[0] + if len(other) > 1 { + d = other[1] + } + } + + return Date(t, year, m, d) +} + +// BeginOfSecond retourne la date au début de la seconde en cours. +func BeginOfSecond(t time.Time) time.Time { return SetNano(t, 0) } + +// EndOfSecond retourne la date à la fin de la seconde en cours. +func EndOfSecond(t time.Time) time.Time { return SetNano(t, 999999999) } + +// BeginOfMinute retourne la date au début de la minute en cours. +func BeginOfMinute(t time.Time) time.Time { return SetSecond(t, 0, 0) } + +// EndOfMinute retourne la date à la fin de la minute en cours. +func EndOfMinute(t time.Time) time.Time { return SetSecond(t, 59, 999999999) } + +// BeginOfHour retourne la date au début de l’heure en cours. +func BeginOfHour(t time.Time) time.Time { return SetMinute(t, 0, 0, 0) } + +// EndOfHour retourne la date à la fin de l’heure en cours. +func EndOfHour(t time.Time) time.Time { return SetMinute(t, 59, 59, 999999999) } + +// BeginOfDay retourne la date au début du jour en cours. +func BeginOfDay(t time.Time) time.Time { return SetHour(t, 0, 0, 0, 0) } + +// EndOfDay retourne la date à la fin du jour en cours. +func EndOfDay(t time.Time) time.Time { return SetHour(t, 23, 59, 59, 999999999) } + +// BeginOfWeek retourne la date au début de la semaine en cours. +func BeginOfWeek(t time.Time) time.Time { + d := t.Weekday() + if d == time.Sunday { + d += 7 + } + + return BeginOfDay(t.AddDate(0, 0, 1-int(d))) +} + +// EndOfWeek retourne la date à la fin de la semaine en cours. +func EndOfWeek(t time.Time) time.Time { + d := t.Weekday() + if d == time.Sunday { + d += 7 + } + + return EndOfDay(t.AddDate(0, 0, 7-int(d))) +} + +// BeginOfMonth retourne la date au début du mois en cours. +func BeginOfMonth(t time.Time) time.Time { return BeginOfDay(SetDay(t, 1)) } + +// EndOfMonth retourne la date à la fin du mois en cours. +func EndOfMonth(t time.Time) time.Time { return EndOfDay(SetDay(t, DaysInMonth(t))) } + +// BeginOfYear retourne la date au début de l’année en cours. +func BeginOfYear(t time.Time) time.Time { return BeginOfDay(SetMonth(t, 1, 1)) } + +// EndOfYear retourne la date à la fin de l’année en cours. +func EndOfYear(t time.Time) time.Time { return EndOfDay(SetMonth(t, 12, 31)) } diff --git a/datetime/parse.go b/datetime/parse.go new file mode 100644 index 0000000..e0d552b --- /dev/null +++ b/datetime/parse.go @@ -0,0 +1,95 @@ +package datetime + +import ( + "fmt" + "strings" + "time" + + . "gitea.zaclys.com/bvaudour/gob/option" +) + +func format2layout(f string) string { + var buffer strings.Builder + + runes := []rune(f) + for i := 0; i < len(runes); i++ { + if layout, ok := parsers[runes[i]]; ok { + buffer.WriteString(layout) + } else { + switch runes[i] { + case '\\': // raw output, no parse + buffer.WriteRune(runes[i+1]) + i++ + continue + default: + buffer.WriteRune(runes[i]) + } + } + } + + return buffer.String() +} + +func parseInLocation(value, layout string, tz ...string) Result[time.Time] { + location := DefaultTZ + if len(tz) > 0 { + var err error + if location, err = time.LoadLocation(tz[0]); err != nil { + return Err[time.Time](fmt.Errorf(errInvalidTZ, tz[0])) + } + } + + t, err := time.ParseInLocation(layout, value, location) + if err == nil { + return Ok(t) + } + return Err[time.Time](fmt.Errorf(errInvalidValue, value)) +} + +// Guess tente de parser la chaîne de caractères en date en essayant de deviner le format. +// Si le fuseau horaire n’est pas précisé dans la chaîne, le fuseau utilisé est tz (si fourni) ou le fuseau horaire par défaut. +func Guess(value string, tz ...string) Result[time.Time] { + if value == "" || value == "0" || value == "0000-00-00 00:00:00" || value == "0000-00-00" || value == "00:00:00" { + return Zero(tz...) + } + + switch value { + case "now": + return Now(tz...) + case "yesterday": + return Yesterday(tz...) + case "tomorrow": + return Tomorrow(tz...) + } + + if len(tz) > 0 { + if _, err := time.LoadLocation(tz[0]); err != nil { + return Err[time.Time](fmt.Errorf(errInvalidTZ, tz[0])) + } + } + + for _, layout := range layouts { + t := parseInLocation(layout, value, tz...) + if t.IsOk() { + return t + } + } + + return Err[time.Time](fmt.Errorf(errInvalidValue, value)) +} + +// ParseFromLayout parse la chaîne de caractères en date à partir du layout (façon Go) fourni. +// Si le fuseau horaire n’est pas précisé dans la chaîne, le fuseau utilisé est tz (si fourni) ou le fuseau horaire par défaut. +func ParseFromLayout(value, layout string, tz ...string) Result[time.Time] { + if value == "" || value == "0" || value == "0000-00-00 00:00:00" || value == "0000-00-00" || value == "00:00:00" { + return Zero(tz...) + } + + return parseInLocation(value, layout, tz...) +} + +// Parse parse la chaîne de caractères en date à partir du format (dans le style PHP) fourni. +// Si le fuseau horaire n’est pas précisé dans la chaîne, le fuseau utilisé est tz (si fourni) ou le fuseau horaire par défaut. +func Parse(value, format string, tz ...string) Result[time.Time] { + return ParseFromLayout(value, format2layout(format), tz...) +} diff --git a/datetime/range.go b/datetime/range.go new file mode 100644 index 0000000..c8c8b54 --- /dev/null +++ b/datetime/range.go @@ -0,0 +1,122 @@ +package datetime + +import ( + "time" + + . "gitea.zaclys.com/bvaudour/gob/option" +) + +// Range représente une période entre deux dates. +type Range struct { + begin time.Time + end time.Time +} + +// NewRange initialise une période. +// Si t1 > t2, la date début de la période sera t2, et inversement si t1 < t2. +func NewRange(t1, t2 time.Time) Range { + if Gt(t1, t2) { + t1, t2 = t2, t1 + } + + return Range{ + begin: t1, + end: t2, + } +} + +// Begin retourne la date de début de la période. +func (r Range) Begin() time.Time { return r.begin } + +// End retourne la date de fin de la période. +func (r Range) End() time.Time { return r.end } + +// BeforeDate retourne vrai si la date est située avant la période. +func (r Range) BeforeDate(t time.Time) bool { return Lt(r.end, t) } + +// AfterDate retourne vrai si la date est située après la période. +func (r Range) AfterDate(t time.Time) bool { return Gt(r.begin, t) } + +// ContainsDate retourne vrai si t ∈ [begin; end]. +func (r Range) ContainsDate(t time.Time) bool { return Le(r.begin, t) && Ge(r.end, t) } + +// ContainsDateStrictBegin retourne vrai si t ∈ ]begin; end]. +func (r Range) ContainsDateStrictBegin(t time.Time) bool { return Lt(r.begin, t) && Ge(r.end, t) } + +// ContainsDateStrictEnd retourne vrai si t ∈ [begin; end[. +func (r Range) ContainsDateStrictEnd(t time.Time) bool { return Le(r.begin, t) && Gt(r.end, t) } + +// ContainsDateStrict retourne vrai si t ∈ ]begin; end[. +func (r Range) ContainsDateStrict(t time.Time) bool { return Lt(r.begin, t) && Gt(r.end, t) } + +// IsFuture retourne vrai si la période est située dans le futur. +func (r Range) IsFuture() bool { return r.AfterDate(time.Now()) } + +// IsPast retourne vrai si la période est située dans le passé. +func (r Range) IsPast() bool { return r.BeforeDate(time.Now()) } + +// IsNow retourne vrai si la période est en cours. +func (r Range) IsNow() bool { return r.ContainsDate(time.Now()) } + +// Before retourne vrai si r est avant r2 sans la recouvrir. +func (r Range) Before(r2 Range) bool { return Le(r.end, r2.begin) } + +// After retourne vrai si r est après r2 sans la recouvrir. +func (r Range) After(r2 Range) bool { return Ge(r.begin, r2.end) } + +// Contains retourne vrai si r inclut complètement r2. +func (r Range) Contains(r2 Range) bool { return Le(r.begin, r2.begin) && Ge(r.end, r2.end) } + +// In retourne vrai si r est complètement inclus dans r2. +func (r Range) In(r2 Range) bool { return r2.Contains(r) } + +// Excludes retourne vrai si les périodes ne se chevauchent pas. +func (r Range) Excludes(r2 Range) bool { return Le(r.end, r2.begin) || Ge(r.begin, r2.end) } + +// Overlaps retourne vrai si les périodes se chevauchent. +func (r Range) Overlaps(r2 Range) bool { return Lt(r.begin, r2.end) && Gt(r.end, r2.begin) } + +// Intersection retourne la période commune aux deux périodes, si elle existe. +func (r Range) Intersection(r2 Range) (result Option[Range]) { + if r.Overlaps(r2) { + result = Some(NewRange(Max(r.begin, r2.begin), Min(r.end, r2.end))) + } + + return +} + +// Joins retourne la plus grande période contiguë entre deux période, si elle existe. +func (r Range) Joins(r2 Range) (result Option[Range]) { + if Le(r.begin, r2.end) && Ge(r.end, r2.begin) { + result = Some(NewRange(Min(r.begin, r2.begin), Max(r.end, r2.end))) + } + + return +} + +// Diff retourne l’ensemble des périodes non communes aux deux périodes. +func (r Range) Diff(r2 Range) (result []Range) { + if Ne(r.begin, r2.begin) { + begin := Min(r.begin, r2.begin) + var end time.Time + if begin == r.begin { + end = Min(r.end, r2.begin) + } else { + end = Min(r2.end, r.begin) + } + result = append(result, NewRange(begin, end)) + } + + if Ne(r.end, r2.end) { + end := Max(r.end, r2.end) + var begin time.Time + if end == r.end { + begin = Max(r.begin, r2.end) + } else { + begin = Max(r2.begin, r.end) + } + result = append(result, NewRange(begin, end)) + } + + return +}