[templating] add humanDateDiff and formatHumanDateDiff functions

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2024-03-30 15:14:30 +01:00
parent bc9c3eeb15
commit e8eb6cd0f4
Signed by: luzifer
SSH Key Fingerprint: SHA256:/xtE5lCgiRDQr8SLxHMS92ZBlACmATUmF1crK16Ks4E
6 changed files with 288 additions and 1 deletions

View File

@ -256,6 +256,19 @@ Example:
< 5 hours, 33 minutes, 12 seconds - 5 hours, 33 minutes
```
### `formatHumanDateDiff`
Formats a DateInterval object according to the format (%Y, %M, %D, %H, %I, %S for years, months, days, hours, minutes, seconds - Lowercase letters without leading zeros)
Syntax: `formatHumanDateDiff <format> <obj>`
Example:
```
# {{ humanDateDiff (mustToDate "2006-01-02 -0700" "2024-05-05 +0200") (mustToDate "2006-01-02 -0700" "2023-01-09 +0100") | formatHumanDateDiff "%Y years, %M months, %D days" }}
< 01 years, 03 months, 25 days
```
### `group`
Gets matching group specified by index from `match_message` regular expression, when `fallback` is defined, it is used when group has an empty match
@ -271,6 +284,19 @@ Example:
< test - oops
```
### `humanDateDiff`
Returns a DateInterval object describing the time difference between a and b in a "human" way of counting the time (2023-02-05 -> 2024-03-05 = 1 Year, 1 Month)
Syntax: `humanDateDiff <a> <b>`
Example:
```
# {{ humanDateDiff (mustToDate "2006-01-02 -0700" "2024-05-05 +0200") (mustToDate "2006-01-02 -0700" "2023-01-09 +0100") }}
< {1 3 25 23 0 0}
```
### `idForUsername`
Returns the user-id for the given username
@ -441,7 +467,7 @@ Example:
```
# Your int this hour: {{ printf "%.0f" (mulf (seededRandom (list "int" .username (now | date "2006-01-02 15") | join ":")) 100) }}%
< Your int this hour: 27%
< Your int this hour: 70%
```
### `spotifyCurrentPlaying`

View File

@ -0,0 +1,31 @@
// Package date adds date-based helper functions for templating
package date
import (
"github.com/Luzifer/twitch-bot/v3/plugins"
)
// Register provides the plugins.RegisterFunc
func Register(args plugins.RegistrationArguments) error {
args.RegisterTemplateFunction("humanDateDiff", plugins.GenericTemplateFunctionGetter(NewInterval), plugins.TemplateFuncDocumentation{
Description: `Returns a DateInterval object describing the time difference between a and b in a "human" way of counting the time (2023-02-05 -> 2024-03-05 = 1 Year, 1 Month)`,
Syntax: "humanDateDiff <a> <b>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ humanDateDiff (mustToDate "2006-01-02 -0700" "2024-05-05 +0200") (mustToDate "2006-01-02 -0700" "2023-01-09 +0100") }}`,
ExpectedOutput: "{1 3 25 23 0 0}",
},
})
args.RegisterTemplateFunction("formatHumanDateDiff", plugins.GenericTemplateFunctionGetter(func(format string, d Interval) string {
return d.Format(format)
}), plugins.TemplateFuncDocumentation{
Description: "Formats a DateInterval object according to the format (%Y, %M, %D, %H, %I, %S for years, months, days, hours, minutes, seconds - Lowercase letters without leading zeros)",
Syntax: "formatHumanDateDiff <format> <obj>",
Example: &plugins.TemplateFuncDocumentationExample{
Template: `{{ humanDateDiff (mustToDate "2006-01-02 -0700" "2024-05-05 +0200") (mustToDate "2006-01-02 -0700" "2023-01-09 +0100") | formatHumanDateDiff "%Y years, %M months, %D days" }}`,
ExpectedOutput: "01 years, 03 months, 25 days",
},
})
return nil
}

View File

@ -0,0 +1,94 @@
package date
import (
"fmt"
"strings"
"time"
)
type (
// Interval represents a human-minded diff of two dates which is in
// no way interchangeable with a time.Duration: The DateInterval
// takes each date component and subtracts them. This causes the
// 03/25 to be exactly one month distant from 02/25 even though the
// distance would be different than with 03/25 and 04/25 which is
// also exactly one month.
Interval struct {
Years int
Months int
Days int
Hours int
Minutes int
Seconds int
}
)
// NewInterval creates an Interval from two given dates
func NewInterval(a, b time.Time) (i Interval) {
var l, u time.Time
if a.Before(b) {
l, u = a.UTC(), b.UTC()
} else {
l, u = b.UTC(), a.UTC()
}
i.Years = u.Year() - l.Year()
i.Months = int(u.Month() - l.Month())
i.Days = u.Day() - l.Day()
i.Hours = u.Hour() - l.Hour()
i.Minutes = u.Minute() - l.Minute()
i.Seconds = u.Second() - l.Second()
if i.Seconds < 0 {
i.Minutes, i.Seconds = i.Minutes-1, i.Seconds+60 //nolint:gomnd
}
if i.Minutes < 0 {
i.Hours, i.Minutes = i.Hours-1, i.Minutes+60 //nolint:gomnd
}
if i.Hours < 0 {
i.Days, i.Hours = i.Days-1, i.Hours+24 //nolint:gomnd
}
if i.Days < 0 {
// oh boi.
i.Months, i.Days = i.Months-1, daysInMonth(u.Year(), int(u.Month())-1)+i.Days
}
if i.Months < 0 {
i.Years, i.Months = i.Years-1, i.Months+12 //nolint:gomnd
}
return i
}
func daysInMonth(year, month int) int {
return time.Date(year, time.Month(month+1), 1, 0, 0, 0, 0, time.Local).Add(-time.Second).Day()
}
// Format takes a template string analog to a strftime string and formats
// the Interval accordingly:
//
// %Y / %y = Years with / without leading digit to 2 places
// %M / %m = Months with / without leading digit to 2 places
// %D / %d = Days with / without leading digit to 2 places
// %H / %h = Hours with / without leading digit to 2 places
// %I / %i = Minutes with / without leading digit to 2 places
// %S / %s = Seconds with / without leading digit to 2 places
func (i Interval) Format(tplString string) string {
return strings.NewReplacer(
"%Y", fmt.Sprintf("%02d", i.Years),
"%y", fmt.Sprintf("%d", i.Years),
"%M", fmt.Sprintf("%02d", i.Months),
"%m", fmt.Sprintf("%d", i.Months),
"%D", fmt.Sprintf("%02d", i.Days),
"%d", fmt.Sprintf("%d", i.Days),
"%H", fmt.Sprintf("%02d", i.Hours),
"%h", fmt.Sprintf("%d", i.Hours),
"%I", fmt.Sprintf("%02d", i.Minutes),
"%i", fmt.Sprintf("%d", i.Minutes),
"%S", fmt.Sprintf("%02d", i.Seconds),
"%s", fmt.Sprintf("%d", i.Seconds),
).Replace(tplString)
}

View File

@ -0,0 +1,134 @@
package date
import (
_ "embed"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
//go:embed tzdata
var tzDataEuropeBerlin []byte
//nolint:funlen // This is just a collection of test cases
func TestNewInterval(t *testing.T) {
tz, err := time.LoadLocationFromTZData("Europe/Berlin", tzDataEuropeBerlin)
require.NoError(t, err)
for i, tc := range []struct {
A, B time.Time
Exp Interval
}{
{
// Plain and simple: 1 Month
A: time.Date(2024, 3, 3, 0, 0, 0, 0, tz),
B: time.Date(2024, 2, 3, 0, 0, 0, 0, tz),
Exp: Interval{0, 1, 0, 0, 0, 0},
},
{
// Plain and simple: 1 Month, reversed
A: time.Date(2024, 2, 3, 0, 0, 0, 0, tz),
B: time.Date(2024, 3, 3, 0, 0, 0, 0, tz),
Exp: Interval{0, 1, 0, 0, 0, 0},
},
{
// Plain and simple: 1 Year, 1 Month
A: time.Date(2023, 2, 3, 0, 0, 0, 0, tz),
B: time.Date(2024, 3, 3, 0, 0, 0, 0, tz),
Exp: Interval{1, 1, 0, 0, 0, 0},
},
{
// 11 Months, so Year and Month needs to be adjusted
A: time.Date(2023, 3, 3, 0, 0, 0, 0, tz),
B: time.Date(2024, 2, 3, 0, 0, 0, 0, tz),
Exp: Interval{0, 11, 0, 0, 0, 0},
},
{
// Plain and simple: 2 Days
A: time.Date(2024, 3, 3, 0, 0, 0, 0, tz),
B: time.Date(2024, 3, 5, 0, 0, 0, 0, tz),
Exp: Interval{0, 0, 2, 0, 0, 0},
},
{
// 1 Month and a few days, so Month and Day needs to be adjusted
A: time.Date(2024, 3, 25, 0, 0, 0, 0, tz),
B: time.Date(2024, 5, 5, 0, 0, 0, 0, tz),
Exp: Interval{0, 1, 9, 23, 0, 0},
},
{
// 1 Month and a few days, so Month and Day needs to be adjusted
A: time.Date(2024, 2, 25, 0, 0, 0, 0, tz),
B: time.Date(2024, 4, 5, 0, 0, 0, 0, tz),
Exp: Interval{0, 1, 10, 23, 0, 0},
},
{
// 1 Month and a few days, so Month and Day needs to be adjusted
A: time.Date(2024, 1, 25, 0, 0, 0, 0, tz),
B: time.Date(2024, 3, 5, 0, 0, 0, 0, tz),
Exp: Interval{0, 1, 9, 0, 0, 0},
},
{
// 1 Month and a few days, so Month and Day needs to be adjusted
A: time.Date(2023, 1, 25, 0, 0, 0, 0, tz),
B: time.Date(2023, 3, 5, 0, 0, 0, 0, tz),
Exp: Interval{0, 1, 8, 0, 0, 0},
},
{
// 1 Day and a few hours, so Day and Hours needs to be adjusted
A: time.Date(2024, 3, 5, 14, 0, 0, 0, tz),
B: time.Date(2024, 3, 7, 0, 0, 0, 0, tz),
Exp: Interval{0, 0, 1, 10, 0, 0},
},
{
// 1 Hour and a few minutes, so Hours and Minutes needs to be adjusted
A: time.Date(2024, 3, 5, 14, 25, 0, 0, tz),
B: time.Date(2024, 3, 5, 16, 12, 0, 0, tz),
Exp: Interval{0, 0, 0, 1, 47, 0},
},
{
// 1 Minute and a few seconds, so Minutes and Seconds needs to be adjusted
A: time.Date(2024, 3, 5, 14, 25, 13, 0, tz),
B: time.Date(2024, 3, 5, 14, 27, 0, 0, tz),
Exp: Interval{0, 0, 0, 0, 1, 47},
},
{
// Nearly one year but a few seconds, everything needs to be adjusted
A: time.Date(2024, 3, 5, 14, 25, 0, 0, tz),
B: time.Date(2023, 3, 5, 14, 25, 13, 0, tz),
Exp: Interval{0, 11, 28, 23, 59, 47},
},
{
// Nearly one year but a few seconds, everything needs to be adjusted
A: time.Date(2024, 8, 5, 14, 25, 0, 0, tz),
B: time.Date(2023, 8, 5, 14, 25, 13, 0, tz),
Exp: Interval{0, 11, 30, 23, 59, 47},
},
{
// Nearly one year but a few seconds, everything needs to be adjusted
A: time.Date(2024, 7, 5, 14, 25, 0, 0, tz),
B: time.Date(2023, 7, 5, 14, 25, 13, 0, tz),
Exp: Interval{0, 11, 29, 23, 59, 47},
},
{
// Nearly one year but a few seconds, everything needs to be adjusted
A: time.Date(2024, 2, 5, 14, 25, 0, 0, tz),
B: time.Date(2023, 2, 5, 14, 25, 13, 0, tz),
Exp: Interval{0, 11, 30, 23, 59, 47},
},
} {
assert.Equal(t,
tc.Exp,
NewInterval(tc.A, tc.B),
fmt.Sprintf("%d: %s -> %s", i, tc.A, tc.B))
}
}
func TestFormatInterval(t *testing.T) {
ti := Interval{1, 2, 3, 4, 5, 6}
assert.Equal(t,
"01 1 years 02 2 months 03 3 days 04 4 hours 05 5 minutes 06 6 seconds",
ti.Format("%Y %y years %M %m months %D %d days %H %h hours %I %i minutes %S %s seconds"))
}

Binary file not shown.

View File

@ -46,6 +46,7 @@ import (
"github.com/Luzifer/twitch-bot/v3/internal/apimodules/raffle"
"github.com/Luzifer/twitch-bot/v3/internal/service/access"
"github.com/Luzifer/twitch-bot/v3/internal/template/api"
"github.com/Luzifer/twitch-bot/v3/internal/template/date"
"github.com/Luzifer/twitch-bot/v3/internal/template/numeric"
"github.com/Luzifer/twitch-bot/v3/internal/template/random"
"github.com/Luzifer/twitch-bot/v3/internal/template/slice"
@ -93,6 +94,7 @@ var (
// Template functions
api.Register,
date.Register,
numeric.Register,
random.Register,
slice.Register,