Further XSD-Date functions

This commit is contained in:
Simon Martens
2024-12-23 18:56:56 +01:00
parent 2f6eef4aa5
commit 0abb3b1ad7
5 changed files with 271 additions and 175 deletions

View File

@@ -1,66 +1,55 @@
package functions package functions
import ( type Month struct {
"time" Full string
) Short string
Number string
const TLAYOUT = "2006-01-02" No int
var TRANSLM = [][]string{
{"Januar", "Jan", "1"},
{"Februar", "Feb", "2"},
{"März", "Mär", "3"},
{"April", "Apr", "4"},
{"Mai", "Mai", "5"},
{"Juni", "Jun", "6"},
{"Juli", "Jul", "7"},
{"August", "Aug", "8"},
{"September", "Sep", "9"},
{"Oktober", "Okt", "10"},
{"November", "Nov", "11"},
{"Dezember", "Dez", "12"},
} }
var TRANSLD = [][]string{ type Weekday struct {
{"Montag", "Mo"}, Full string
{"Dienstag", "Di"}, Short string
{"Mittwoch", "Mi"}, No int
{"Donnerstag", "Do"},
{"Freitag", "Fr"},
{"Samstag", "Sa"},
{"Sonntag", "So"},
} }
type Date struct { var TRANSLM = []Month{
Month string {"NotAvailable", "NA", "0", 0},
Mon string {"Januar", "Jan", "1", 1},
MonthNo string {"Februar", "Feb", "2", 2},
DayNo int {"März", "Mär", "3", 3},
Weekday string {"April", "Apr", "4", 4},
Wd string {"Mai", "Mai", "5", 5},
{"Juni", "Jun", "6", 6},
{"Juli", "Jul", "7", 7},
{"August", "Aug", "8", 8},
{"September", "Sep", "9", 9},
{"Oktober", "Okt", "10", 10},
{"November", "Nov", "11", 11},
{"Dezember", "Dez", "12", 12},
} }
func GetDate(d string) Date { var TRANSLD = []Weekday{
t, err := time.Parse(TLAYOUT, d) {"NotAvailable", "NA", 0},
if err != nil { {"Montag", "Mo", 1},
return Date{} {"Dienstag", "Di", 2},
{"Mittwoch", "Mi", 3},
{"Donnerstag", "Do", 4},
{"Freitag", "Fr", 5},
{"Samstag", "Sa", 6},
{"Sonntag", "So", 7},
}
func MonthName(i int) Month {
if i > 12 || i < 1 {
return TRANSLM[0]
} }
m := int(t.Month()) - 1 return TRANSLM[i]
wd := int(t.Weekday()) - 1 }
return Date{
Month: TRANSLM[m][0], func WeekdayName(i int) Weekday {
Mon: TRANSLM[m][1], if i > 7 || i < 1 {
MonthNo: TRANSLM[m][2], return TRANSLD[0]
DayNo: t.Day(),
Weekday: TRANSLD[wd][0],
Wd: TRANSLD[wd][1],
} }
} return TRANSLD[i]
func MonthName(m int) string {
return TRANSLM[m-1][0]
}
func MonthNameShort(m int) string {
return TRANSLM[m-1][1]
} }

View File

@@ -1,10 +1,10 @@
package xsdtime package xsdtime
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time"
) )
// An implementation of the xsd 1.1 datatypes: // An implementation of the xsd 1.1 datatypes:
@@ -29,7 +29,9 @@ const (
) )
const ( const (
Date XSDDatetype = iota Unknown XSDDatetype = iota
Invalid
Date
GDay GDay
GMonth GMonth
GYear GYear
@@ -38,15 +40,27 @@ const (
) )
type XSDDate struct { type XSDDate struct {
Year int base string
Month int
Day int
Timezone int
Type XSDDatetype Year int
HasTimezone bool Month int
Day int
Time time.Time hasTimezone bool
hasYear bool
hasMonth bool
hasDay bool
TZH int
TZM int
state XSDDatetype
error bool
// INFO: XSD Date Datatypes typically describe a duration in the value space.
// TimeError bool
// BaseTime time.Time
// BaseDuration time.Duration
} }
// Sanity check: // Sanity check:
@@ -64,6 +78,12 @@ type XSDDate struct {
// - no 0000 Year // - no 0000 Year
// //
func New(s string) (XSDDate, error) {
dt := XSDDate{base: s}
err := dt.Parse(s)
return dt, err
}
func (d XSDDate) String() string { func (d XSDDate) String() string {
var s string var s string
if d.Year != 0 { if d.Year != 0 {
@@ -84,24 +104,19 @@ func (d XSDDate) String() string {
s += fmt.Sprintf("-%02d", d.Day) s += fmt.Sprintf("-%02d", d.Day)
} }
if d.HasTimezone { if d.hasTimezone {
if d.Timezone == 0 { if d.TZH == 0 && d.TZM == 0 {
s += "Z" s += "Z"
} else { } else {
m := d.Timezone % 60
if m < 0 {
m *= -1
}
hint := d.Timezone / 60
sep := "+" sep := "+"
hint := d.TZH
if hint < 0 { if hint < 0 {
sep = "-" sep = "-"
hint *= -1 hint *= -1
} }
h := fmt.Sprintf("%02d", hint) h := fmt.Sprintf("%02d", hint)
s += fmt.Sprintf("%v%v:%02d", sep, h, m) s += fmt.Sprintf("%v%v:%02d", sep, h, d.TZM)
} }
} }
@@ -109,48 +124,29 @@ func (d XSDDate) String() string {
} }
func (d *XSDDate) UnmarshalText(text []byte) error { func (d *XSDDate) UnmarshalText(text []byte) error {
dt, err := Parse(string(text)) return d.Parse(string(text))
if err != nil {
return err
}
d.Year = dt.Year
d.Month = dt.Month
d.Day = dt.Day
d.Timezone = dt.Timezone
d.Type = dt.Type
d.HasTimezone = dt.HasTimezone
return nil
} }
func (d XSDDate) MarshalText() ([]byte, error) { func (d XSDDate) MarshalText() ([]byte, error) {
return []byte(d.String()), nil return []byte(d.String()), nil
} }
func Parse(s string) (XSDDate, error) { func (xsdd *XSDDate) Parse(s string) error {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
xsdd.base = s
// The smallest possible date is 4 chars long // The smallest possible date is 4 chars long
if len(s) < 4 { if len(s) < 4 {
return XSDDate{}, fmt.Errorf("Invalid date") return xsdd.parseError("Date too short")
} }
y := 0 // Check for Z, then check for timezone
m := 0
d := 0
hastz := false
tz := 0
if len(s) >= 5 && s[len(s)-1] == TIMEZONE { if len(s) >= 5 && s[len(s)-1] == TIMEZONE {
hastz = true xsdd.hasTimezone = true
tz = 0
s = s[:len(s)-1] s = s[:len(s)-1]
} else if len(s) >= 10 { } else if len(s) >= 10 {
t, err := parseTimezone(s[len(s)-6:]) err := xsdd.parseTimezone(s[len(s)-6:])
if err == nil { if err == nil {
hastz = true
tz = t
s = s[:len(s)-6] s = s[:len(s)-6]
} }
} }
@@ -159,23 +155,20 @@ func Parse(s string) (XSDDate, error) {
if s[1] != SEPERATOR { if s[1] != SEPERATOR {
i := 3 i := 3
for ; i < len(s); i++ { for ; i < len(s); i++ {
if !isAllowed(s[i]) { if s[i] < MIN_ALLOWED_NUMBER || s[i] > MAX_ALLOWED_NUMBER {
break break
} }
} }
yint, err := strconv.Atoi(s[:i]) yint, err := strconv.Atoi(s[:i])
if err != nil { if err != nil {
return XSDDate{}, fmt.Errorf("Invalid year: %v", s[:i]) return xsdd.parseError(fmt.Sprintf("Invalid year: %v", s[:i]))
} else if yint == 0 {
return XSDDate{}, fmt.Errorf("Zero is an invalid year")
} }
y = yint xsdd.Year = yint
xsdd.hasYear = true
if i == len(s) { if i == len(s) {
return XSDDate{Year: y, Type: GYear, Timezone: tz, HasTimezone: hastz}, nil return nil
} else if i >= len(s)-1 || s[i] != SEPERATOR {
return XSDDate{}, fmt.Errorf("Invalid date ending")
} }
s = s[i+1:] s = s[i+1:]
@@ -188,23 +181,16 @@ func Parse(s string) (XSDDate, error) {
mstr := s[:2] mstr := s[:2]
mint, err := strconv.Atoi(mstr) mint, err := strconv.Atoi(mstr)
if err != nil { if err != nil {
return XSDDate{}, fmt.Errorf("Invalid month") return xsdd.parseError(fmt.Sprintf("Invalid month: %v", mstr))
} }
if mint < 1 || mint > 12 { xsdd.Month = mint
return XSDDate{}, fmt.Errorf("Invalid month value") xsdd.hasMonth = true
}
m = mint
s = s[2:] s = s[2:]
if len(s) == 0 { if len(s) == 0 {
if y == 0 { return nil
return XSDDate{Month: m, Type: GMonth, HasTimezone: hastz, Timezone: tz}, nil
} else {
return XSDDate{Year: y, Month: m, Type: GYearMonth, HasTimezone: hastz, Timezone: tz}, nil
}
} else if len(s) != 3 || s[0] != SEPERATOR { } else if len(s) != 3 || s[0] != SEPERATOR {
return XSDDate{}, fmt.Errorf("Invalid date ending: %v", s) return xsdd.parseError(fmt.Sprintf("Invalid date ending: %v", s))
} }
} }
@@ -213,59 +199,173 @@ func Parse(s string) (XSDDate, error) {
// Left is 02 Day // Left is 02 Day
dint, err := strconv.Atoi(s) dint, err := strconv.Atoi(s)
if err != nil { if err != nil {
return XSDDate{}, fmt.Errorf("Invalid day: %v", s) return xsdd.parseError(fmt.Sprintf("Invalid day: %v", s))
} }
if dint < 1 || dint > 31 { // INFO: We do not check len here, it is handled above
return XSDDate{}, fmt.Errorf("Invalid day value: %v", dint) xsdd.Day = dint
} xsdd.hasDay = true
d = dint return nil
if y == 0 {
if m == 0 {
return XSDDate{Day: d, Type: GDay, HasTimezone: hastz, Timezone: tz}, nil
} else {
return XSDDate{Month: m, Day: d, Type: GMonthDay, HasTimezone: hastz, Timezone: tz}, nil
}
} else {
return XSDDate{Year: y, Month: m, Day: d, Type: Date, HasTimezone: hastz, Timezone: tz}, nil
}
} }
func parseTimezone(s string) (int, error) { var WD_CALC_MATRIX = []int{0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}
func (xsdd XSDDate) Weekday() int {
y := xsdd.Year
if xsdd.Month < 3 {
y--
}
return (y + y/4 - y/100 + y/400 + WD_CALC_MATRIX[xsdd.Month-1] + xsdd.Day) % 7
}
func (xsdd XSDDate) Base() string {
return xsdd.base
}
func (xsdd XSDDate) Type() XSDDatetype {
if xsdd.state == Unknown {
_ = xsdd.Validate()
}
return xsdd.state
}
func (xsdd *XSDDate) Validate() bool {
if xsdd.error {
xsdd.state = Invalid
return false
}
xsdd.state = xsdd.inferState()
if xsdd.state != Invalid {
return true
}
return false
}
func (xsdd *XSDDate) parseError(s string) error {
xsdd.error = true
xsdd.state = Invalid
return errors.New(s)
}
func (xsdd *XSDDate) parseTimezone(s string) error {
// INFO: We assume the check for 'Z' has already been done // INFO: We assume the check for 'Z' has already been done
if len(s) != 6 || s[3] != COLON || (s[0] != PLUS && s[0] != SIGN) { if len(s) != 6 || s[3] != COLON || (s[0] != PLUS && s[0] != SIGN) {
return 0, fmt.Errorf("Invalid timezone") return fmt.Errorf("Invalid timezone")
} }
h, err := strconv.Atoi(s[:3]) h, err := strconv.Atoi(s[:3])
if err != nil { if err != nil {
return 0, fmt.Errorf("Invalid hour: %v", s[:3]) return fmt.Errorf("Invalid hour: %v", s[:3])
} }
m, err := strconv.Atoi(s[4:]) m, err := strconv.Atoi(s[4:])
if err != nil { if err != nil {
return 0, fmt.Errorf("Invalid minute: %v", s[4:]) return fmt.Errorf("Invalid minute: %v", s[4:])
} }
if (h < -13 || h > 13) && ((h == -14 || h == 14) && m != 0) { xsdd.hasTimezone = true
return 0, fmt.Errorf("Invalid timezone: hour: %v minute: %v", h, m) xsdd.TZH = h
} xsdd.TZM = m
if m < 0 || m > 59 { return nil
return 0, fmt.Errorf("Invalid timezone: minute: %v", m)
}
h *= 60
if h < 0 {
h -= m
} else {
h += m
}
return h, nil
} }
func isAllowed(c byte) bool { func (xsdd XSDDate) inferState() XSDDatetype {
return c >= MIN_ALLOWED_NUMBER && c <= MAX_ALLOWED_NUMBER if xsdd.hasYear && xsdd.hasMonth && xsdd.hasDay {
if !validDayMonthYear(xsdd.Year, xsdd.Month, xsdd.Day) {
return Invalid
}
return Date
} else if xsdd.hasYear && xsdd.hasMonth {
if !validMonth(xsdd.Month) || !validYear(xsdd.Year) {
return Invalid
}
return GYearMonth
} else if xsdd.hasMonth && xsdd.hasDay {
if !validDayMonth(xsdd.Day, xsdd.Month) {
return Invalid
}
return GMonthDay
} else if xsdd.hasYear {
if !validYear(xsdd.Year) {
return Invalid
}
return GYear
} else if xsdd.hasMonth {
if !validMonth(xsdd.Month) {
return Invalid
}
return GMonth
} else if xsdd.hasDay {
if !validDay(xsdd.Day) {
return Invalid
}
return GDay
}
return Invalid
}
func validDay(i int) bool {
if i < 1 || i > 31 {
return false
}
return true
}
func validMonth(i int) bool {
if i < 1 || i > 12 {
return false
}
return true
}
func validYear(i int) bool {
if i == 0 {
return false
}
return true
}
func validDayMonth(d int, m int) bool {
if !validDay(d) || !validMonth(m) {
return false
}
if m == 2 {
if d > 29 {
return false
}
} else if m == 4 || m == 6 || m == 9 || m == 11 {
if d > 30 {
return false
}
}
return true
}
func validDayMonthYear(y int, m int, d int) bool {
if !validDay(d) || !validMonth(m) || !validYear(y) {
return false
}
if m == 2 {
if d == 29 {
if y%4 == 0 && (y%100 != 0 || y%400 == 0) {
return true
}
return false
}
}
return true
} }

View File

@@ -5,55 +5,63 @@ import "testing"
type Test struct { type Test struct {
Input string Input string
Output XSDDate Output XSDDate
Type XSDDatetype
} }
var tests = []Test{ var tests = []Test{
{"2006-01-02", XSDDate{Year: 2006, Month: 1, Day: 2, Type: Date}}, {"2006-01-02", XSDDate{Year: 2006, Month: 1, Day: 2}, GYear},
{"-1222-01-02", XSDDate{Year: -1222, Month: 1, Day: 2, Type: Date}}, {"-1222-01-02", XSDDate{Year: -1222, Month: 1, Day: 2}, Date},
{"-2777", XSDDate{Year: -2777, Type: GYear}}, {"-2777", XSDDate{Year: -2777}, GYear},
{"1988-12:30", XSDDate{Year: 1988, Type: GYear, HasTimezone: true, Timezone: (60*12 + 30) * -1}}, {"1988-12:30", XSDDate{Year: 1988, hasTimezone: true, TZH: -12, TZM: 30}, GYear},
{"--03+05:00", XSDDate{Month: 3, Type: GMonth, HasTimezone: true, Timezone: 300}}, {"--03+05:00", XSDDate{Month: 3, hasTimezone: true, TZH: 5, TZM: 0}, GMonth},
{"---29", XSDDate{Day: 29, Type: GDay}}, {"---29", XSDDate{Day: 29}, GDay},
{"-1234567-12Z", XSDDate{Year: -1234567, Month: 12, Type: GYearMonth, HasTimezone: true, Timezone: 0}}, {"-1234567-12Z", XSDDate{Year: -1234567, Month: 12, hasTimezone: true, TZH: 0, TZM: 0}, GYearMonth},
{"-1234567-12+05:00", XSDDate{Year: -1234567, Month: 12, Type: GYearMonth, HasTimezone: true, Timezone: 300}}, {"-1234567-12+05:00", XSDDate{Year: -1234567, Month: 12, hasTimezone: true, TZH: 5, TZM: 0}, GYearMonth},
{"--12-31", XSDDate{Month: 12, Day: 31, Type: GMonthDay}}, {"--12-31", XSDDate{Month: 12, Day: 31}, GMonthDay},
} }
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
for _, test := range tests { for _, test := range tests {
dt, err := Parse(test.Input) dt, err := New(test.Input)
if err != nil { if err != nil {
t.Errorf("Error parsing %v: %v", test.Input, err) t.Errorf("Error parsing %v: %v", test.Input, err)
continue continue
} }
if dt.Type != test.Output.Type {
t.Errorf("Type mismatch for %v: expected %v, got %v", test.Input, test.Output.Type, dt.Type)
}
if dt.Year != test.Output.Year { if dt.Year != test.Output.Year {
t.Errorf("Year mismatch for %v: expected %v, got %v", test.Input, test.Output.Year, dt.Year) t.Errorf("Year mismatch for %v: expected %v, got %v", test.Input, test.Output.Year, dt.Year)
} }
if dt.Month != test.Output.Month { if dt.Month != test.Output.Month {
t.Errorf("Month mismatch for %v: expected %v, got %v", test.Input, test.Output.Month, dt.Month) t.Errorf("Month mismatch for %v: expected %v, got %v", test.Input, test.Output.Month, dt.Month)
} }
if dt.Day != test.Output.Day { if dt.Day != test.Output.Day {
t.Errorf("Day mismatch for %v: expected %v, got %v", test.Input, test.Output.Day, dt.Day) t.Errorf("Day mismatch for %v: expected %v, got %v", test.Input, test.Output.Day, dt.Day)
} }
if dt.HasTimezone != test.Output.HasTimezone {
t.Errorf("Timezone mismatch for %v: expected %v, got %v", test.Input, test.Output.HasTimezone, dt.HasTimezone) if dt.hasTimezone != test.Output.hasTimezone {
t.Errorf("Timezone mismatch for %v: expected %v, got %v", test.Input, test.Output.hasTimezone, dt.hasTimezone)
} }
if dt.Timezone != test.Output.Timezone {
t.Errorf("Timezone mismatch for %v: expected %v, got %v", test.Input, test.Output.Timezone, dt.Timezone) if dt.TZH != test.Output.TZH {
t.Errorf("Timezone mismatch for %v: expected %v, got %v", test.Input, test.Output.TZH, dt.TZH)
}
if dt.TZM != test.Output.TZM {
t.Errorf("Timezone mismatch for %v: expected %v, got %v", test.Input, test.Output.TZM, dt.TZM)
} }
} }
} }
func TestString(t *testing.T) { func TestString(t *testing.T) {
for _, test := range tests { for _, test := range tests {
dt, err := Parse(test.Input) dt, err := New(test.Input)
if err != nil { if err != nil {
t.Errorf("Error parsing %v: %v", test.Input, err) t.Errorf("Error parsing %v: %v", test.Input, err)
continue continue
} }
if dt.String() != test.Input { if dt.String() != test.Input {
t.Errorf("String mismatch for %v: expected %v, got %v", test.Input, test.Input, dt.String()) t.Errorf("String mismatch for %v: expected %v, got %v", test.Input, test.Input, dt.String())
} }

View File

@@ -38,9 +38,8 @@ func (e *Engine) Funcs(app *app.KGPZ) error {
e.FuncMap = make(map[string]interface{}) e.FuncMap = make(map[string]interface{})
e.mu.Unlock() e.mu.Unlock()
e.AddFunc("GetDate", functions.GetDate)
e.AddFunc("MonthName", functions.MonthName) e.AddFunc("MonthName", functions.MonthName)
e.AddFunc("MonthNameShort", functions.MonthNameShort) e.AddFunc("WeekdayName", functions.WeekdayName)
e.AddFunc("GetAgent", app.Library.Agents.Item) e.AddFunc("GetAgent", app.Library.Agents.Item)
e.AddFunc("GetPlace", app.Library.Places.Item) e.AddFunc("GetPlace", app.Library.Places.Item)

View File

@@ -9,7 +9,7 @@
<!-- Issues --> <!-- Issues -->
{{ range $issue := $month }} {{ range $issue := $month }}
{{ $date := GetDate $issue.Datum.When }} {{ $date := GetDate $issue.Datum.When.String }}
<a href="/{{ $y }}/{{ $issue.Number.No }}"> <a href="/{{ $y }}/{{ $issue.Number.No }}">
<div> <div>
{{ $issue.Number.No }} {{ $issue.Number.No }}