lunes.go (349 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package lunes import ( "errors" "fmt" "strings" "time" "unicode" ) var longDayNamesStd = []string{ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", } var shortDayNamesStd = []string{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", } var shortMonthNamesStd = []string{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", } var longMonthNamesStd = []string{ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", } var dayPeriodsStdUpper = []string{ "AM", "PM", } var dayPeriodsStdLower = []string{ "am", "pm", } // Parse parses a formatted string in foreign language and returns the [time.Time] value // it represents. See the documentation for the constant called [time.Layout] to see how to // represent the format. // // After translating the foreign language value to English, it gets the time value by // calling the Go standard [time.Parse] function. // // The language argument must be a well-formed BCP 47 language tag, e.g ("en", "en-US") and // a known locale. If no data is found for the language, it returns ErrUnsupportedLocale. // If the given language does not support any [time.Layout] element specified on the layout // argument, it results in an ErrUnsupportedLayoutElem error. On the other hand, if the value // does not match the layout, an ErrLayoutMismatch is returned. See the documentation for // [time.Parse] for other possible errors it might return. // // To execute several parses for the same locale, use [ParseWithLocale] as it performs better. func Parse(layout string, value string, lang string) (time.Time, error) { locale, err := NewDefaultLocale(lang) if err != nil { return time.Time{}, err } return ParseWithLocale(layout, value, locale) } // ParseWithLocale is like Parse, but instead of receiving a BCP 47 language tag argument, // it receives a built [lunes.Locale], avoiding looking up existing data in each operation // and allowing extensibility. func ParseWithLocale(layout string, value string, locale Locale) (time.Time, error) { pv, err := TranslateWithLocale(layout, value, locale) if err != nil { return time.Time{}, err } return time.Parse(layout, pv) } // ParseInLocation is like Parse, but it interprets the time as in the given location. // In addition to the [Parse] errors, it might return any [time.ParseInLocation] possible errors. // To execute several parses for the same locale, use [ParseInLocationWithLocale] as it performs better. func ParseInLocation(layout string, value string, lang string, location *time.Location) (time.Time, error) { locale, err := NewDefaultLocale(lang) if err != nil { return time.Time{}, err } return ParseInLocationWithLocale(layout, value, location, locale) } // ParseInLocationWithLocale is like ParseInLocation, but instead of receiving a BCP 47 // language tag argument, it receives a built [lunes.Locale], avoiding looking up existing // data in each operation and allowing extensibility. func ParseInLocationWithLocale(layout string, value string, location *time.Location, locale Locale) (time.Time, error) { pv, err := TranslateWithLocale(layout, value, locale) if err != nil { return time.Time{}, err } return time.ParseInLocation(layout, pv, location) } // Translate parses a localized textual time value from the provided locale to English. // It replaces short and long week days names, months names, and day periods by their // equivalents. The first argument must be a native Go time layout. The second argument // must be parseable using the format string (layout) provided as the first argument, // but in the foreign language. The language argument must be a well-formed BCP 47 // language tag, e.g ("en", "en-US") and a known locale. If no data is found for the // language, it returns ErrUnsupportedLocale. // // If the given locale does not support a layout element specified on the layout argument, // it results in an ErrUnsupportedLayoutElem error. On the other hand, if the value does // not match the layout, an ErrLayoutMismatch is returned. // // This function is meant to return a value that can be used with the Go standard // [time.Parse] or [time.ParseInLocation] methods. Although it maintains value's empty // spaces that are not present in the layout string, it might drop them in the future, // as they are ignored by both standard time parsings functions. func Translate(layout string, value string, lang string) (string, error) { locale, err := NewDefaultLocale(lang) if err != nil { return value, err } return TranslateWithLocale(layout, value, locale) } // TranslateWithLocale is like Translate, but instead of receiving a BCP 47 language tag // argument, it receives a built [lunes.Locale], avoiding looking up existing data in each // operation and allowing extensibility. func TranslateWithLocale(layout string, value string, locale Locale) (string, error) { var err error var sb strings.Builder var layoutOffset, valueOffset int sb.Grow(len(layout) + 32) for layoutOffset < len(layout) { written := false var lookupTab, stdTab []string switch c := int(layout[layoutOffset]); c { case 'J': // January, Jan if len(layout) >= layoutOffset+3 && layout[layoutOffset:layoutOffset+3] == "Jan" { layoutElem := "" if len(layout) >= layoutOffset+7 && layout[layoutOffset:layoutOffset+7] == "January" { layoutElem = "January" lookupTab = locale.LongMonthNames() stdTab = longMonthNamesStd } else if !startsWithLowerCase(layout[layoutOffset+3:]) { layoutElem = "Jan" lookupTab = locale.ShortMonthNames() stdTab = shortMonthNamesStd } if layoutElem == "" { break } if len(lookupTab) == 0 { return "", newUnsupportedLayoutElemError(layoutElem, locale) } layoutOffset += len(layoutElem) valueOffset, err = writeLayoutValue(layoutElem, lookupTab, stdTab, valueOffset, value, &sb) if err != nil { return "", err } written = true } case 'M': // Monday, Mon if len(layout) >= layoutOffset+3 && layout[layoutOffset:layoutOffset+3] == "Mon" { layoutElem := "" if len(layout) >= layoutOffset+6 && layout[layoutOffset:layoutOffset+6] == "Monday" { layoutElem = "Monday" lookupTab = locale.LongDayNames() stdTab = longDayNamesStd } else if !startsWithLowerCase(layout[layoutOffset+3:]) { layoutElem = "Mon" lookupTab = locale.ShortDayNames() stdTab = shortDayNamesStd } if layoutElem == "" { break } if len(lookupTab) == 0 { return "", newUnsupportedLayoutElemError(layoutElem, locale) } layoutOffset += len(layoutElem) valueOffset, err = writeLayoutValue(layoutElem, lookupTab, stdTab, valueOffset, value, &sb) if err != nil { return "", err } written = true } case 'P', 'p': // PM, pm if len(layout) >= layoutOffset+2 && unicode.ToUpper(rune(layout[layoutOffset+1])) == 'M' { var layoutElem string // day-periods case matters for the time package parsing functions if c == 'p' { layoutElem = "pm" stdTab = dayPeriodsStdLower } else { layoutElem = "PM" stdTab = dayPeriodsStdUpper } lookupTab = locale.DayPeriods() if len(lookupTab) == 0 { return "", newUnsupportedLayoutElemError(layoutElem, locale) } layoutOffset += 2 valueOffset, err = writeLayoutValue(layoutElem, lookupTab, stdTab, valueOffset, value, &sb) if err != nil { return "", err } written = true } case '_': // _2, _2006, __2 // Although no translations happens here, it is still necessary to calculate the // variable size of `_` values, so the layoutOffset stays synchronized with // its layout element counterpart. if len(layout) >= layoutOffset+2 && layout[layoutOffset+1] == '2' { var layoutElemSize int // _2006 is really a literal _, followed by the long year placeholder if len(layout) >= layoutOffset+5 && layout[layoutOffset+1:layoutOffset+5] == "2006" { if len(value) >= valueOffset+5 { layoutElemSize = 5 // _2006 } } else { if len(value) >= valueOffset+2 { layoutElemSize = 2 // _2 } } if layoutElemSize > 0 { layoutOffset += layoutElemSize valueOffset, err = writeNextNonSpaceValue(value, valueOffset, layoutElemSize, &sb) if err != nil { return "", err } written = true } } if len(layout) >= layoutOffset+3 && layout[layoutOffset+1] == '_' && layout[layoutOffset+2] == '2' { if len(value) >= valueOffset+3 { layoutOffset += 3 valueOffset, err = writeNextNonSpaceValue(value, valueOffset, 3, &sb) if err != nil { return "", err } written = true } } } if !written { var writtenSize int if len(value) > valueOffset { writtenSize, err = sb.WriteRune(rune(value[valueOffset])) if err != nil { return "", err } } layoutOffset++ valueOffset += writtenSize } } if len(value) >= valueOffset { sb.WriteString(value[valueOffset:]) } return sb.String(), nil } func writeNextNonSpaceValue(value string, offset int, max int, sb *strings.Builder) (int, error) { nextValOffset, skippedSpaces, val, err := nextNonSpaceValue(value, offset, max) if err != nil { return offset, err } if skippedSpaces > 0 { val = strings.Repeat(" ", skippedSpaces) + val } _, err = sb.WriteString(val) if err != nil { return offset, err } return nextValOffset, nil } func writeLayoutValue(layoutElem string, lookupTab, stdTab []string, valueOffset int, value string, sb *strings.Builder) (int, error) { newOffset, skippedSpaces, foundStdValue, val := lookup(lookupTab, valueOffset, value, stdTab) if foundStdValue == "" { return valueOffset, newLayoutMismatchError(layoutElem, value) } if skippedSpaces > 0 { foundStdValue = strings.Repeat(" ", skippedSpaces) + foundStdValue } _, err := sb.WriteString(foundStdValue) if err != nil { return valueOffset, err } newOffset += len(val) return newOffset, nil } func nextNonSpaceValue(value string, offset int, max int) (newOffset, skippedSpaces int, val string, err error) { newOffset = offset for newOffset < len(value) && unicode.IsSpace(rune(value[newOffset])) { newOffset++ } skippedSpaces = newOffset - offset if newOffset > len(value) { return offset, skippedSpaces, "", errors.New("next non-space value not found") } for newOffset < len(value) { if !unicode.IsSpace(rune(value[newOffset])) { val += string(value[newOffset]) newOffset++ } else { return newOffset, skippedSpaces, val, nil } if len(val) == max { return newOffset, skippedSpaces, val, nil } } return newOffset, skippedSpaces, val, nil } func lookup(lookupTab []string, offset int, val string, stdTab []string) (newOffset, skippedSpaces int, stdValue string, value string) { newOffset = offset for newOffset < len(val) && unicode.IsSpace(rune(val[newOffset])) { newOffset++ } skippedSpaces = newOffset - offset if newOffset > len(val) { return offset, skippedSpaces, "", val } for i, v := range lookupTab { // Already matched a more specific/longer value if stdValue != "" && len(v) <= len(value) { continue } end := newOffset + len(v) if end > len(val) { continue } candidate := val[newOffset:end] if len(candidate) == len(v) && strings.EqualFold(candidate, v) { stdValue = stdTab[i] value = candidate } } return newOffset, skippedSpaces, stdValue, value } func startsWithLowerCase(value string) bool { if len(value) == 0 { return false } c := value[0] return 'a' <= c && c <= 'z' } // ErrLayoutMismatch indicates that a provided value does not match its layout counterpart. type ErrLayoutMismatch struct { Value string LayoutElem string } func (l *ErrLayoutMismatch) Error() string { return fmt.Sprintf(`value "%s" does not match the layout element "%s"`, l.Value, l.LayoutElem) } func (l *ErrLayoutMismatch) Is(err error) bool { var target *ErrLayoutMismatch if ok := errors.As(err, &target); ok { return l.Value == target.Value && l.LayoutElem == target.LayoutElem } return false } func newLayoutMismatchError(elem, value string) error { return &ErrLayoutMismatch{ LayoutElem: elem, Value: value, } } // ErrUnsupportedLayoutElem indicates that a provided layout element is not supported by // the given locale/language. type ErrUnsupportedLayoutElem struct { LayoutElem string Language string } func (u *ErrUnsupportedLayoutElem) Error() string { return fmt.Sprintf(`layout element "%s" is not support by the language "%s"`, u.LayoutElem, u.Language) } func (u *ErrUnsupportedLayoutElem) Is(err error) bool { var target *ErrUnsupportedLayoutElem if ok := errors.As(err, &target); ok { return u.Language == target.Language && u.LayoutElem == target.LayoutElem } return false } func newUnsupportedLayoutElemError(elem string, locale Locale) error { return &ErrUnsupportedLayoutElem{ LayoutElem: elem, Language: locale.Language(), } }