1
0
Fork 0

Adding upstream version 2.5.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-19 00:20:02 +02:00
parent c71cb8b61d
commit 982828099e
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
783 changed files with 150650 additions and 0 deletions

View file

@ -0,0 +1,205 @@
// Copyright (c) 2023 Couchbase, Inc.
//
// Licensed 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 percent
import (
"fmt"
"strings"
"time"
"github.com/blevesearch/bleve/v2/analysis"
"github.com/blevesearch/bleve/v2/registry"
)
const Name = "percentstyle"
var formatDelimiter byte = '%'
// format specifiers as per strftime in the C standard library
// https://man7.org/linux/man-pages/man3/strftime.3.html
var formatSpecifierToLayout = map[byte]string{
formatDelimiter: string(formatDelimiter), // %% = % (literal %)
'a': "Mon", // %a = short weekday name
'A': "Monday", // %A = full weekday name
'd': "02", // %d = day of month (2 digits) (01-31)
'e': "2", // %e = day of month (1 digit) (1-31)
'b': "Jan", // %b = short month name
'B': "January", // %B = full month name
'm': "01", // %m = month of year (2 digits) (01-12)
'y': "06", // %y = year without century
'Y': "2006", // %Y = year with century
'H': "15", // %H = hour (24 hour clock) (2 digits)
'I': "03", // %I = hour (12 hour clock) (2 digits)
'l': "3", // %l = hour (12 hour clock) (1 digit)
'p': "PM", // %p = PM/AM
'P': "pm", // %P = pm/am (lowercase)
'M': "04", // %M = minute (2 digits)
'S': "05", // %S = seconds (2 digits)
'f': "999999", // .%f = fraction of seconds - up to microseconds (6 digits) - deci/milli/micro
'Z': "MST", // %Z = timezone name (GMT, JST, UTC etc)
// %z is present in timezone options
// some additional options not in strftime to support additional options such as
// disallow 0 padding in minute and seconds, nanosecond precision, etc
'o': "1", // %o = month of year (1 digit) (1-12)
'i': "4", // %i = minute (1 digit)
's': "5", // %s = seconds (1 digit)
'N': "999999999", // .%N = fraction of seconds - up to microseconds (9 digits) - milli/micro/nano
}
// some additional options for timezone
// such as allowing colon in timezone offset and specifying the seconds
// timezone offsets are from UTC
var timezoneOptions = map[string]string{
"z": "Z0700", // %z = timezone offset in +-hhmm / +-(2 digit hour)(2 digit minute) +0500, -0600 etc
"z:M": "Z07:00", // %z:M = timezone offset(+-hh:mm) / +-(2 digit hour):(2 digit minute) +05:00, -06:00 etc
"z:S": "Z07:00:00", // %z:M = timezone offset(+-hh:mm:ss) / +-(2 digit hour):(2 digit minute):(2 digit second) +05:20:00, -06:30:00 etc
"zH": "Z07", // %zH = timezone offset(+-hh) / +-(2 digit hour) +05, -06 etc
"zS": "Z070000", // %zS = timezone offset(+-hhmmss) / +-(2 digit hour)(2 digit minute)(2 digit second) +052000, -063000 etc
}
type DateTimeParser struct {
layouts []string
}
func New(layouts []string) *DateTimeParser {
return &DateTimeParser{
layouts: layouts,
}
}
func checkTZOptions(formatString string, idx int) (string, int) {
// idx points to '%'
// We know formatString[idx+1] == 'z'
nextIdx := idx + 2 // Index of the character immediately after 'z'
// Default values assume only '%z' is present
layout := timezoneOptions["z"]
finalIdx := nextIdx // Index after '%z'
if nextIdx < len(formatString) {
switch formatString[nextIdx] {
case ':':
// Check for modifier after the colon ':'
colonModifierIdx := nextIdx + 1
if colonModifierIdx < len(formatString) {
switch formatString[colonModifierIdx] {
case 'M':
// Found %z:M
layout = timezoneOptions["z:M"]
finalIdx = colonModifierIdx + 1 // Index after %z:M
case 'S':
// Found %z:S
layout = timezoneOptions["z:S"]
finalIdx = colonModifierIdx + 1 // Index after %z:S
// default: If %z: is followed by something else, or just %z: at the end.
// Keep the default layout ("z") and finalIdx (idx + 2).
// The ':' will be treated as a literal by the main loop.
}
}
// else: %z: is at the very end of the string.
// Keep the default layout ("z") and finalIdx (idx + 2).
// The ':' will be treated as a literal by the main loop.
case 'H':
// Found %zH
layout = timezoneOptions["zH"]
finalIdx = nextIdx + 1 // Index after %zH
case 'S':
// Found %zS
layout = timezoneOptions["zS"]
finalIdx = nextIdx + 1 // Index after %zS
// default: If %z is followed by something other than ':', 'H', or 'S'.
// Keep the default layout ("z") and finalIdx (idx + 2).
// The character formatString[nextIdx] will be handled by the main loop.
}
}
// else: %z is at the very end of the string.
// Keep the default layout ("z") and finalIdx (idx + 2).
return layout, finalIdx
}
func parseFormatString(formatString string) (string, error) {
var dateTimeLayout strings.Builder
// iterate over the format string and replace the format specifiers with
// the corresponding golang constants
for idx := 0; idx < len(formatString); {
// check if the character is a format delimiter (%)
if formatString[idx] == formatDelimiter {
// check if there is a character after the format delimiter (%)
if idx+1 >= len(formatString) {
return "", fmt.Errorf("invalid format string, expected character after %s", string(formatDelimiter))
}
formatSpecifier := formatString[idx+1]
if layout, ok := formatSpecifierToLayout[formatSpecifier]; ok {
dateTimeLayout.WriteString(layout)
idx += 2
} else if formatSpecifier == 'z' {
// did not find a valid specifier
// check if it is for timezone
var tzLayout string
tzLayout, idx = checkTZOptions(formatString, idx)
dateTimeLayout.WriteString(tzLayout)
} else {
return "", fmt.Errorf("invalid format string, unknown format specifier: %s", string(formatSpecifier))
}
continue
}
// copy the character as is
dateTimeLayout.WriteByte(formatString[idx])
idx++
}
return dateTimeLayout.String(), nil
}
func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) {
for _, layout := range p.layouts {
rv, err := time.Parse(layout, input)
if err == nil {
return rv, layout, nil
}
}
return time.Time{}, "", analysis.ErrInvalidDateTime
}
func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
layouts, ok := config["layouts"].([]interface{})
if !ok {
return nil, fmt.Errorf("must specify layouts")
}
layoutStrs := make([]string, 0, len(layouts))
for _, layout := range layouts {
layoutStr, ok := layout.(string)
if ok {
layout, err := parseFormatString(layoutStr)
if err != nil {
return nil, err
}
layoutStrs = append(layoutStrs, layout)
}
}
return New(layoutStrs), nil
}
func init() {
err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
if err != nil {
panic(err)
}
}

View file

@ -0,0 +1,474 @@
// Copyright (c) 2023 Couchbase, Inc.
//
// Licensed 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 percent
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/blevesearch/bleve/v2/analysis"
)
func TestConversionFromPercentStyle(t *testing.T) {
tests := []struct {
name string // Added name field
input string
output string
err error
}{
{
name: "basic YMD",
input: "%Y-%m-%d",
output: "2006-01-02",
err: nil,
},
{
name: "YMD with double percent and literal T",
input: "%Y/%m%%%%%dT%H%M:%S",
output: "2006/01%%02T1504:05",
err: nil,
},
{
name: "YMD T HMS Z z",
input: "%Y-%m-%dT%H:%M:%S %Z%z",
output: "2006-01-02T15:04:05 MSTZ0700",
err: nil,
},
{
name: "Full month, padded day/hour, am/pm, z:M",
input: "%B %e, %Y %l:%i %P %z:M",
output: "January 2, 2006 3:4 pm Z07:00",
err: nil,
},
{
name: "Long format with literals and timezone literal :S",
input: "Hour %H Minute %Mseconds %S.%N Timezone:%Z:S, Weekday %a; Day %d Month %b, Year %y",
output: "Hour 15 Minute 04seconds 05.999999999 Timezone:MST:S, Weekday Mon; Day 02 Month Jan, Year 06",
err: nil,
},
{
name: "YMD T HMS with nanoseconds",
input: "%Y-%m-%dT%H:%M:%S.%N",
output: "2006-01-02T15:04:05.999999999",
err: nil,
},
{
name: "HMS Z z",
input: "%H:%M:%S %Z %z",
output: "15:04:05 MST Z0700",
err: nil,
},
{
name: "HMS Z z literal colon",
input: "%H:%M:%S %Z %z:",
output: "15:04:05 MST Z0700:",
err: nil,
},
{
name: "HMS Z z:M",
input: "%H:%M:%S %Z %z:M",
output: "15:04:05 MST Z07:00",
err: nil,
},
{
name: "HMS Z z:S",
input: "%H:%M:%S %Z %z:S",
output: "15:04:05 MST Z07:00:00",
err: nil,
},
{
name: "HMS Z z: literal A",
input: "%H:%M:%S %Z %z:A",
output: "15:04:05 MST Z0700:A",
err: nil,
},
{
name: "HMS Z z literal M",
input: "%H:%M:%S %Z %zM",
output: "15:04:05 MST Z0700M",
err: nil,
},
{
name: "HMS Z zH",
input: "%H:%M:%S %Z %zH",
output: "15:04:05 MST Z07",
err: nil,
},
{
name: "HMS Z zS",
input: "%H:%M:%S %Z %zS",
output: "15:04:05 MST Z070000",
err: nil,
},
{
name: "Complex combination z zS z: zH",
input: "%H:%M:%S %Z %z%Z %zS%z:%zH",
output: "15:04:05 MST Z0700MST Z070000Z0700:Z07",
err: nil,
},
{
name: "z at end",
input: "%Y-%m-%d %z",
output: "2006-01-02 Z0700",
err: nil,
},
{
name: "z: at end",
input: "%Y-%m-%d %z:",
output: "2006-01-02 Z0700:",
err: nil,
},
{
name: "zH at end",
input: "%Y-%m-%d %zH",
output: "2006-01-02 Z07",
err: nil,
},
{
name: "zS at end",
input: "%Y-%m-%d %zS",
output: "2006-01-02 Z070000",
err: nil,
},
{
name: "z:M at end",
input: "%Y-%m-%d %z:M",
output: "2006-01-02 Z07:00",
err: nil,
},
{
name: "z:S at end",
input: "%Y-%m-%d %z:S",
output: "2006-01-02 Z07:00:00",
err: nil,
},
{
name: "z followed by literal X",
input: "%Y-%m-%d %zX",
output: "2006-01-02 Z0700X",
err: nil,
},
{
name: "z: followed by literal X",
input: "%Y-%m-%d %z:X",
output: "2006-01-02 Z0700:X",
err: nil,
},
{
name: "Invalid specifier T",
input: "%Y-%m-%d%T%H:%M:%S %ZM",
output: "",
err: fmt.Errorf("invalid format string, unknown format specifier: T"),
},
{
name: "Ends with %",
input: "%Y-%m-%dT%H:%M:%S %ZM%",
output: "",
err: fmt.Errorf("invalid format string, expected character after %%"),
},
{
name: "Just %",
input: "%",
output: "",
err: fmt.Errorf("invalid format string, expected character after %%"),
},
{
name: "Just %%",
input: "%%",
output: "%",
err: nil,
},
{
name: "Unknown specifier x",
input: "%x",
output: "",
err: fmt.Errorf("invalid format string, unknown format specifier: x"),
},
{
name: "Literal prefix",
input: "literal %Y",
output: "literal 2006",
err: nil,
},
{
name: "Literal suffix",
input: "%Y literal",
output: "2006 literal",
err: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
out, err := parseFormatString(test.input)
// Enhanced Error Check:
expectedErrStr := ""
if test.err != nil {
expectedErrStr = test.err.Error()
}
actualErrStr := ""
if err != nil {
actualErrStr = err.Error()
}
if expectedErrStr != actualErrStr {
// Provide more detailed output if errors don't match as strings
t.Fatalf("error mismatch:\nExpected error: %q\nGot error : %q", expectedErrStr, actualErrStr)
}
// Original error presence check (redundant if string check passes, but safe to keep)
if (err != nil && test.err == nil) || (err == nil && test.err != nil) {
t.Fatalf("presence mismatch: expected error %v, got error %v", test.err, err)
}
// Check output matching only if no error was expected/occurred
if err == nil && test.err == nil && out != test.output {
t.Fatalf("output mismatch: expected '%v', got '%v'", test.output, out)
}
})
}
}
func TestDateTimeParser_ParseDateTime(t *testing.T) {
// Pre-create some parsers with known Go layouts
parser1 := New([]string{"2006-01-02", "01/02/2006"}) // YYYY-MM-DD, MM/DD/YYYY
parser2 := New([]string{"15:04:05"}) // HH:MM:SS
parserEmpty := New([]string{}) // No layouts
// Define expected time values
time1, _ := time.Parse("2006-01-02", "2023-10-27")
time2, _ := time.Parse("01/02/2006", "10/27/2023")
time3, _ := time.Parse("15:04:05", "14:30:00")
tests := []struct {
name string
parser *DateTimeParser
input string
expectTime time.Time
expectLayout string
expectErr error
}{
{
name: "match first layout",
parser: parser1,
input: "2023-10-27",
expectTime: time1,
expectLayout: "2006-01-02",
expectErr: nil,
},
{
name: "match second layout",
parser: parser1,
input: "10/27/2023",
expectTime: time2,
expectLayout: "01/02/2006",
expectErr: nil,
},
{
name: "no matching layout",
parser: parser1,
input: "14:30:00", // Matches parser2's layout, not parser1's
expectTime: time.Time{},
expectLayout: "",
expectErr: analysis.ErrInvalidDateTime,
},
{
name: "match only layout",
parser: parser2,
input: "14:30:00",
expectTime: time3,
expectLayout: "15:04:05",
expectErr: nil,
},
{
name: "invalid date format for layout",
parser: parser1,
input: "27-10-2023", // Wrong separators
expectTime: time.Time{},
expectLayout: "",
expectErr: analysis.ErrInvalidDateTime, // time.Parse fails on all, returns ErrInvalidDateTime
},
{
name: "empty input",
parser: parser1,
input: "",
expectTime: time.Time{},
expectLayout: "",
expectErr: analysis.ErrInvalidDateTime,
},
{
name: "parser with no layouts",
parser: parserEmpty,
input: "2023-10-27",
expectTime: time.Time{},
expectLayout: "",
expectErr: analysis.ErrInvalidDateTime,
},
{
name: "not a date string",
parser: parser1,
input: "hello world",
expectTime: time.Time{},
expectLayout: "",
expectErr: analysis.ErrInvalidDateTime,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotTime, gotLayout, gotErr := test.parser.ParseDateTime(test.input)
// Check error
if !reflect.DeepEqual(gotErr, test.expectErr) {
t.Fatalf("error mismatch:\nExpected: %v\nGot: %v", test.expectErr, gotErr)
}
// Check time only if no error expected
if test.expectErr == nil {
if !gotTime.Equal(test.expectTime) {
t.Errorf("time mismatch:\nExpected: %v\nGot: %v", test.expectTime, gotTime)
}
if gotLayout != test.expectLayout {
t.Errorf("layout mismatch:\nExpected: %q\nGot: %q", test.expectLayout, gotLayout)
}
}
})
}
}
func TestDateTimeParserConstructor(t *testing.T) {
tests := []struct {
name string
config map[string]interface{}
expectLayouts []string // Expected Go layouts after parsing
expectErr error
}{
{
name: "valid config with multiple layouts",
config: map[string]interface{}{
"layouts": []interface{}{"%Y-%m-%d", "%H:%M:%S %Z"},
},
expectLayouts: []string{"2006-01-02", "15:04:05 MST"},
expectErr: nil,
},
{
name: "valid config with single layout",
config: map[string]interface{}{
"layouts": []interface{}{"%Y/%m/%d %z:M"},
},
expectLayouts: []string{"2006/01/02 Z07:00"},
expectErr: nil,
},
{
name: "valid config with complex layout",
config: map[string]interface{}{
"layouts": []interface{}{"%a, %d %b %Y %H:%M:%S %zH"},
},
expectLayouts: []string{"Mon, 02 Jan 2006 15:04:05 Z07"},
expectErr: nil,
},
{
name: "config missing layouts key",
config: map[string]interface{}{
"other_key": "value",
},
expectLayouts: nil,
expectErr: fmt.Errorf("must specify layouts"),
},
{
name: "config layouts not a slice",
config: map[string]interface{}{
"layouts": "not-a-slice", // Value is a string
},
expectLayouts: nil,
// Update the expected error message
expectErr: fmt.Errorf("must specify layouts"),
},
{
name: "config layouts contains non-string",
config: map[string]interface{}{
"layouts": []interface{}{"%Y-%m-%d", 123},
},
// Should process the valid string, ignore the int
expectLayouts: []string{"2006-01-02"},
expectErr: nil,
},
{
name: "config layouts contains invalid percent format",
config: map[string]interface{}{
"layouts": []interface{}{"%Y-%m-%d", "%x"}, // %x is invalid
},
expectLayouts: nil,
expectErr: fmt.Errorf("invalid format string, unknown format specifier: x"),
},
{
name: "config layouts contains format ending in %",
config: map[string]interface{}{
"layouts": []interface{}{"%Y-%m-%d", "%H:%M:%"},
},
expectLayouts: nil,
expectErr: fmt.Errorf("invalid format string, expected character after %%"),
},
{
name: "config with empty layouts slice",
config: map[string]interface{}{
"layouts": []interface{}{},
},
expectLayouts: []string{}, // Expect an empty slice, not nil
expectErr: nil,
},
{
name: "nil config",
config: nil,
expectLayouts: nil,
expectErr: fmt.Errorf("must specify layouts"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Cache is not used by this constructor, so nil is fine
parserIntf, err := DateTimeParserConstructor(test.config, nil)
// Check error
// Use string comparison for errors as they might be created differently
expectedErrStr := ""
if test.expectErr != nil {
expectedErrStr = test.expectErr.Error()
}
actualErrStr := ""
if err != nil {
actualErrStr = err.Error()
}
if expectedErrStr != actualErrStr {
t.Fatalf("error mismatch:\nExpected: %q\nGot: %q", expectedErrStr, actualErrStr)
}
// Check layouts only if no error expected
if test.expectErr == nil {
// Type assert to access the layouts field
parser, ok := parserIntf.(*DateTimeParser)
if !ok {
t.Fatalf("constructor did not return a *DateTimeParser")
}
if !reflect.DeepEqual(parser.layouts, test.expectLayouts) {
t.Errorf("layouts mismatch:\nExpected: %v\nGot: %v", test.expectLayouts, parser.layouts)
}
}
})
}
}