Skip to content

Commit

Permalink
feat(template): add date template helper functions (#5997)
Browse files Browse the repository at this point in the history
* feat: add date template helper functions

Signed-off-by: Manuel Ruck <git@manuelruck.de>

* refactor: separate `modifyDate` and `shiftDate` functions

The hierarchical switch statements are hard to maintain.

The `mode` argument served as a routing param
to pick up necessary function behaviour.

It's better to get rid of it and to maintain 2 independent functions.
Each function can have its own limitations and time unit types.

* improvement: type-safe time units

TODO:
 * consider binding `ShiftDateTimeUnit` to `Duration` from `date-fns`
 * add more tests

* test: tests and docs for `modifyDate` function

* test: tests and docs for `shiftDate` function

* fix: remove unsupported time unit from `shiftDate`

Milliseconds are not supported by the underlying `add` function.

* test: enable test for `formatDate`

* test: mute failing test

* refactor: replace switch-statement with lookup-index

* chore: rename some functions args

* feat: stick date modifiers to UTC TZ

* feat: stick date formatter to UTC TZ

* chore: use arrow-type instead of `Function` in type declaration

Type `Function` is not recommended to use by `@typescript-eslint/ban-types`.
Let's use a bit more specific function interface.

* chore: use `unknown` instead of `any` if possible

* chore: added tests and examples with TZ for hour modifiers

---------

Signed-off-by: Manuel Ruck <git@manuelruck.de>
Co-authored-by: Manuel Ruck <git@manuelruck.de>
Co-authored-by: Vladimir Vagaytsev <vladimir.vagaitsev@gmail.com>
  • Loading branch information
3 people committed May 16, 2024
1 parent 7d8034b commit 39d2396
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 4 deletions.
1 change: 1 addition & 0 deletions core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"types": "build/src/index.d.ts",
"dependencies": {
"@codenamize/codenamize": "^1.1.1",
"@date-fns/utc": "^1.2.0",
"@hapi/joi": "git+https://github.com/garden-io/joi.git#master",
"@jsdevtools/readdir-enhanced": "^6.0.4",
"@kubernetes/client-node": "^1.0.0-rc4",
Expand Down
127 changes: 127 additions & 0 deletions core/src/template-string/date-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright (C) 2018-2024 Garden Technologies, Inc. <info@garden.io>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import type { TemplateHelperFunction } from "./functions.js"
import { joi } from "../config/common.js"
import { format as formatFns, add, type Duration } from "date-fns"
import { UTCDateMini } from "@date-fns/utc"

type ShiftDateTimeUnit = keyof Duration
const validShiftDateTimeUnits: ShiftDateTimeUnit[] = [
"years",
"months",
"weeks",
"days",
"hours",
"minutes",
"seconds",
] as const

const validModifyDateTimeUnits = ["years", "months", "days", "hours", "minutes", "seconds", "milliseconds"] as const
type ModifyDateTimeUnit = (typeof validModifyDateTimeUnits)[number]
// This is still type-safe because every entry of ModifyDateTimeUnit must be declared in the index below.
const modifyDateFunctions: { [k in ModifyDateTimeUnit]: (date: Date, timeUnits: number) => void } = {
years: (date, timeUnits) => date.setUTCFullYear(timeUnits),
months: (date, timeUnits) => date.setUTCMonth(timeUnits),
days: (date, timeUnits) => date.setUTCDate(timeUnits),
hours: (date, timeUnits) => date.setUTCHours(timeUnits),
minutes: (date, timeUnits) => date.setUTCMinutes(timeUnits),
seconds: (date, timeUnits) => date.setUTCSeconds(timeUnits),
milliseconds: (date, timeUnits) => date.setUTCMilliseconds(timeUnits),
} as const

const timeZoneComment =
"The input date is always converted to the UTC time zone before the modification. If no explicit timezone is specified on the input date, then the system default one will be used. The output date is always returned in the UTC time zone too."

export const dateHelperFunctionSpecs: TemplateHelperFunction[] = [
{
name: "formatDateUtc",
description: `Formats the given date using the specified format. ${timeZoneComment}`,
arguments: {
date: joi.string().required().description("The date to format."),
format: joi
.string()
.required()
.description("The format to use. See https://date-fns.org/v2.21.1/docs/format for details."),
},
outputSchema: joi.string(),
exampleArguments: [
{ input: ["2021-01-01T00:00:00Z", "yyyy-MM-dd"], output: "2021-01-01" },
{ input: ["2021-01-01T00:00:00+0200", "yyyy-MM-dd"], output: "2020-12-31" },
{ input: ["2021-01-01T00:00:00Z", "yyyy-MM-dd HH:mm:ss"], output: "2021-01-01 00:00:00" },
{ input: ["2021-01-01T00:00:00+0200", "yyyy-MM-dd HH:mm:ss"], output: "2020-12-31 22:00:00" },
],
fn: (date: string, format: string) => {
const utcDate = new UTCDateMini(date)
return formatFns(utcDate, format)
},
},
{
name: "shiftDateUtc",
description: `Shifts the date by the specified amount of time units. ${timeZoneComment}`,
arguments: {
date: joi.string().required().description("The date to shift."),
amount: joi.number().required().description("The amount of time units to shift the date by."),
unit: joi
.string()
.valid(...validShiftDateTimeUnits)
.required()
.description("The time unit to shift the date by."),
},
outputSchema: joi.string(),
exampleArguments: [
{ input: ["2021-01-01T00:00:00Z", 1, "seconds"], output: "2021-01-01T00:00:01.000Z" },
{ input: ["2021-01-01T00:00:00Z", -1, "seconds"], output: "2020-12-31T23:59:59.000Z" },
{ input: ["2021-01-01T00:00:00Z", 1, "minutes"], output: "2021-01-01T00:01:00.000Z" },
{ input: ["2021-01-01T00:00:00Z", -1, "minutes"], output: "2020-12-31T23:59:00.000Z" },
{ input: ["2021-01-01T00:00:00Z", 1, "hours"], output: "2021-01-01T01:00:00.000Z" },
{ input: ["2021-01-01T00:00:00Z", -1, "hours"], output: "2020-12-31T23:00:00.000Z" },
{ input: ["2021-01-01T10:00:00+0200", 1, "hours"], output: "2021-01-01T09:00:00.000Z" },
{ input: ["2021-01-01T00:00:00Z", 1, "days"], output: "2021-01-02T00:00:00.000Z" },
{ input: ["2021-01-01T00:00:00Z", -1, "days"], output: "2020-12-31T00:00:00.000Z" },
{ input: ["2021-01-01T00:00:00Z", 1, "months"], output: "2021-02-01T00:00:00.000Z" },
{ input: ["2021-01-01T00:00:00Z", -1, "months"], output: "2020-12-01T00:00:00.000Z" },
{ input: ["2021-01-01T00:00:00Z", 1, "years"], output: "2022-01-01T00:00:00.000Z" },
{ input: ["2021-01-01T00:00:00Z", -1, "years"], output: "2020-01-01T00:00:00.000Z" },
],
fn: (date: string, timeUnitAmount: number, unit: ShiftDateTimeUnit) => {
const dateClone = new Date(date)
return add(dateClone, { [unit]: timeUnitAmount }).toISOString()
},
},
{
name: "modifyDateUtc",
description: `Modifies the date by setting the specified amount of time units. ${timeZoneComment}`,
arguments: {
date: joi.string().required().description("The date to modify."),
amount: joi.number().required().description("The amount of time units to set."),
unit: joi
.string()
.valid(...validModifyDateTimeUnits)
.required()
.description("The time unit to set."),
},
outputSchema: joi.string(),
exampleArguments: [
{ input: ["2021-01-01T00:00:00.234Z", 345, "milliseconds"], output: "2021-01-01T00:00:00.345Z" },
{ input: ["2021-01-01T00:00:05Z", 30, "seconds"], output: "2021-01-01T00:00:30.000Z" },
{ input: ["2021-01-01T00:01:00Z", 15, "minutes"], output: "2021-01-01T00:15:00.000Z" },
{ input: ["2021-01-01T12:00:00Z", 11, "hours"], output: "2021-01-01T11:00:00.000Z" },
{ input: ["2021-01-01T10:00:00+0200", 11, "hours"], output: "2021-01-01T11:00:00.000Z" },
{ input: ["2021-01-31T00:00:00Z", 1, "days"], output: "2021-01-01T00:00:00.000Z" },
{ input: ["2021-03-01T00:00:00Z", 0, "months"], output: "2021-01-01T00:00:00.000Z" }, // 0 (Jan) - 11 (Dec)
{ input: ["2021-01-01T00:00:00Z", 2024, "years"], output: "2024-01-01T00:00:00.000Z" },
],
fn: (date: string, timeUnitAmount: number, unit: ModifyDateTimeUnit) => {
const dateClone = new Date(date)
const dateModifier = modifyDateFunctions[unit]
dateModifier(dateClone, timeUnitAmount)
return dateClone.toISOString()
},
},
]
10 changes: 6 additions & 4 deletions core/src/template-string/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,21 @@ import { load, loadAll } from "js-yaml"
import { safeDumpYaml } from "../util/serialization.js"
import indentString from "indent-string"
import { mayContainTemplateString } from "./template-string.js"
import { dateHelperFunctionSpecs } from "./date-functions.js"

interface ExampleArgument {
input: any[]
output: any // Used to validate expected output
input: unknown[]
output: unknown // Used to validate expected output
skipTest?: boolean
}

interface TemplateHelperFunction {
export interface TemplateHelperFunction {
name: string
description: string
arguments: { [name: string]: Joi.Schema }
outputSchema: Joi.Schema
exampleArguments: ExampleArgument[]
fn: Function
fn: (...args: any[]) => unknown
}

const helperFunctionSpecs: TemplateHelperFunction[] = [
Expand Down Expand Up @@ -407,6 +408,7 @@ const helperFunctionSpecs: TemplateHelperFunction[] = [
}
},
},
...dateHelperFunctionSpecs,
]

interface ResolvedHelperFunction extends TemplateHelperFunction {
Expand Down
52 changes: 52 additions & 0 deletions docs/reference/template-strings/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ Examples:
* `${concat([1,2,3], [4,5])}` -> `[1,2,3,4,5]`
* `${concat("string1", "string2")}` -> `"string1string2"`

## formatDateUtc

Formats the given date using the specified format. The input date is always converted to the UTC time zone before the modification. If no explicit timezone is specified on the input date, then the system default one will be used. The output date is always returned in the UTC time zone too.

Usage: `formatDateUtc(date, format)`

Examples:

* `${formatDateUtc("2021-01-01T00:00:00Z", "yyyy-MM-dd")}` -> `"2021-01-01"`
* `${formatDateUtc("2021-01-01T00:00:00+0200", "yyyy-MM-dd")}` -> `"2020-12-31"`
* `${formatDateUtc("2021-01-01T00:00:00Z", "yyyy-MM-dd HH:mm:ss")}` -> `"2021-01-01 00:00:00"`
* `${formatDateUtc("2021-01-01T00:00:00+0200", "yyyy-MM-dd HH:mm:ss")}` -> `"2020-12-31 22:00:00"`

## indent

Indents each line in the given string with the specified number of spaces.
Expand Down Expand Up @@ -134,6 +147,23 @@ Examples:

* `${lower("Some String")}` -> `"some string"`

## modifyDateUtc

Modifies the date by setting the specified amount of time units. The input date is always converted to the UTC time zone before the modification. If no explicit timezone is specified on the input date, then the system default one will be used. The output date is always returned in the UTC time zone too.

Usage: `modifyDateUtc(date, amount, unit)`

Examples:

* `${modifyDateUtc("2021-01-01T00:00:00.234Z", 345, "milliseconds")}` -> `"2021-01-01T00:00:00.345Z"`
* `${modifyDateUtc("2021-01-01T00:00:05Z", 30, "seconds")}` -> `"2021-01-01T00:00:30.000Z"`
* `${modifyDateUtc("2021-01-01T00:01:00Z", 15, "minutes")}` -> `"2021-01-01T00:15:00.000Z"`
* `${modifyDateUtc("2021-01-01T12:00:00Z", 11, "hours")}` -> `"2021-01-01T11:00:00.000Z"`
* `${modifyDateUtc("2021-01-01T10:00:00+0200", 11, "hours")}` -> `"2021-01-01T11:00:00.000Z"`
* `${modifyDateUtc("2021-01-31T00:00:00Z", 1, "days")}` -> `"2021-01-01T00:00:00.000Z"`
* `${modifyDateUtc("2021-03-01T00:00:00Z", 0, "months")}` -> `"2021-01-01T00:00:00.000Z"`
* `${modifyDateUtc("2021-01-01T00:00:00Z", 2024, "years")}` -> `"2024-01-01T00:00:00.000Z"`

## replace

Replaces all occurrences of a given substring in a string.
Expand All @@ -155,6 +185,28 @@ Examples:

* `${sha256("Some String")}` -> `"7f0fd64653ba0bb1a579ced2b6bf375e916cc60662109ee0c0b24f0a750c3a6c"`

## shiftDateUtc

Shifts the date by the specified amount of time units. The input date is always converted to the UTC time zone before the modification. If no explicit timezone is specified on the input date, then the system default one will be used. The output date is always returned in the UTC time zone too.

Usage: `shiftDateUtc(date, amount, unit)`

Examples:

* `${shiftDateUtc("2021-01-01T00:00:00Z", 1, "seconds")}` -> `"2021-01-01T00:00:01.000Z"`
* `${shiftDateUtc("2021-01-01T00:00:00Z", -1, "seconds")}` -> `"2020-12-31T23:59:59.000Z"`
* `${shiftDateUtc("2021-01-01T00:00:00Z", 1, "minutes")}` -> `"2021-01-01T00:01:00.000Z"`
* `${shiftDateUtc("2021-01-01T00:00:00Z", -1, "minutes")}` -> `"2020-12-31T23:59:00.000Z"`
* `${shiftDateUtc("2021-01-01T00:00:00Z", 1, "hours")}` -> `"2021-01-01T01:00:00.000Z"`
* `${shiftDateUtc("2021-01-01T00:00:00Z", -1, "hours")}` -> `"2020-12-31T23:00:00.000Z"`
* `${shiftDateUtc("2021-01-01T10:00:00+0200", 1, "hours")}` -> `"2021-01-01T09:00:00.000Z"`
* `${shiftDateUtc("2021-01-01T00:00:00Z", 1, "days")}` -> `"2021-01-02T00:00:00.000Z"`
* `${shiftDateUtc("2021-01-01T00:00:00Z", -1, "days")}` -> `"2020-12-31T00:00:00.000Z"`
* `${shiftDateUtc("2021-01-01T00:00:00Z", 1, "months")}` -> `"2021-02-01T00:00:00.000Z"`
* `${shiftDateUtc("2021-01-01T00:00:00Z", -1, "months")}` -> `"2020-12-01T00:00:00.000Z"`
* `${shiftDateUtc("2021-01-01T00:00:00Z", 1, "years")}` -> `"2022-01-01T00:00:00.000Z"`
* `${shiftDateUtc("2021-01-01T00:00:00Z", -1, "years")}` -> `"2020-01-01T00:00:00.000Z"`

## slice

Slices a string or array at the specified start/end offsets. Note that you can use a negative number for the end offset to count backwards from the end.
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 39d2396

Please sign in to comment.