snapshot of https://github.com/blevesearch/bleve.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Coverage / coverage (push) Failing after 6s
Tests / test (1.24.x, ubuntu-latest) (push) Failing after 42s
Tests / test (1.26.x, ubuntu-latest) (push) Failing after 38s
Tests / test (1.25.x, ubuntu-latest) (push) Failing after 35s
Tests / test (1.25.x, windows-latest) (push) Has been cancelled
Tests / test (1.26.x, macos-latest) (push) Has been cancelled
Tests / test (1.26.x, windows-latest) (push) Has been cancelled
Tests / test (1.24.x, macos-latest) (push) Has been cancelled
Tests / test (1.24.x, windows-latest) (push) Has been cancelled
Tests / test (1.25.x, macos-latest) (push) Has been cancelled
Some checks failed
Coverage / coverage (push) Failing after 6s
Tests / test (1.24.x, ubuntu-latest) (push) Failing after 42s
Tests / test (1.26.x, ubuntu-latest) (push) Failing after 38s
Tests / test (1.25.x, ubuntu-latest) (push) Failing after 35s
Tests / test (1.25.x, windows-latest) (push) Has been cancelled
Tests / test (1.26.x, macos-latest) (push) Has been cancelled
Tests / test (1.26.x, windows-latest) (push) Has been cancelled
Tests / test (1.24.x, macos-latest) (push) Has been cancelled
Tests / test (1.24.x, windows-latest) (push) Has been cancelled
Tests / test (1.25.x, macos-latest) (push) Has been cancelled
This commit is contained in:
67
analysis/datetime/flexible/flexible.go
Normal file
67
analysis/datetime/flexible/flexible.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2014 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 flexible
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
)
|
||||
|
||||
const Name = "flexiblego"
|
||||
|
||||
type DateTimeParser struct {
|
||||
layouts []string
|
||||
}
|
||||
|
||||
func New(layouts []string) *DateTimeParser {
|
||||
return &DateTimeParser{
|
||||
layouts: layouts,
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
var layoutStrs []string
|
||||
for _, layout := range layouts {
|
||||
layoutStr, ok := layout.(string)
|
||||
if ok {
|
||||
layoutStrs = append(layoutStrs, layoutStr)
|
||||
}
|
||||
}
|
||||
return New(layoutStrs), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
100
analysis/datetime/flexible/flexible_test.go
Normal file
100
analysis/datetime/flexible/flexible_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2014 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 flexible
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
)
|
||||
|
||||
func TestFlexibleDateTimeParser(t *testing.T) {
|
||||
testLocation := time.FixedZone("", -8*60*60)
|
||||
|
||||
rfc3339NoTimezone := "2006-01-02T15:04:05"
|
||||
rfc3339NoTimezoneNoT := "2006-01-02 15:04:05"
|
||||
rfc3339NoTime := "2006-01-02"
|
||||
|
||||
dateOptionalTimeParser := New(
|
||||
[]string{
|
||||
time.RFC3339Nano,
|
||||
time.RFC3339,
|
||||
rfc3339NoTimezone,
|
||||
rfc3339NoTimezoneNoT,
|
||||
rfc3339NoTime,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
expectedTime time.Time
|
||||
expectedLayout string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
input: "2014-08-03",
|
||||
expectedTime: time.Date(2014, 8, 3, 0, 0, 0, 0, time.UTC),
|
||||
expectedLayout: rfc3339NoTime,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
input: "2014-08-03T15:59:30",
|
||||
expectedTime: time.Date(2014, 8, 3, 15, 59, 30, 0, time.UTC),
|
||||
expectedLayout: rfc3339NoTimezone,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
input: "2014-08-03 15:59:30",
|
||||
expectedTime: time.Date(2014, 8, 3, 15, 59, 30, 0, time.UTC),
|
||||
expectedLayout: rfc3339NoTimezoneNoT,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
input: "2014-08-03T15:59:30-08:00",
|
||||
expectedTime: time.Date(2014, 8, 3, 15, 59, 30, 0, testLocation),
|
||||
expectedLayout: time.RFC3339Nano,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
|
||||
input: "2014-08-03T15:59:30.999999999-08:00",
|
||||
expectedTime: time.Date(2014, 8, 3, 15, 59, 30, 999999999, testLocation),
|
||||
expectedLayout: time.RFC3339Nano,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
input: "not a date time",
|
||||
expectedTime: time.Time{},
|
||||
expectedLayout: "",
|
||||
expectedError: analysis.ErrInvalidDateTime,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
actualTime, actualLayout, actualErr := dateOptionalTimeParser.ParseDateTime(test.input)
|
||||
if actualErr != test.expectedError {
|
||||
t.Fatalf("expected error %#v, got %#v", test.expectedError, actualErr)
|
||||
}
|
||||
if !reflect.DeepEqual(actualTime, test.expectedTime) {
|
||||
t.Errorf("expected time %v, got %v", test.expectedTime, actualTime)
|
||||
}
|
||||
if !reflect.DeepEqual(actualLayout, test.expectedLayout) {
|
||||
t.Errorf("expected layout %v, got %v", test.expectedLayout, actualLayout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
250
analysis/datetime/iso/iso.go
Normal file
250
analysis/datetime/iso/iso.go
Normal file
@@ -0,0 +1,250 @@
|
||||
// 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 iso
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
)
|
||||
|
||||
const Name = "isostyle"
|
||||
|
||||
var textLiteralDelimiter byte = '\'' // single quote
|
||||
|
||||
// ISO style date strings are represented in
|
||||
// https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html
|
||||
//
|
||||
// Some format specifiers are not specified in go time package, such as:
|
||||
// - 'V' for timezone name, like 'Europe/Berlin' or 'America/New_York'.
|
||||
// - 'Q' for quarter of year, like Q3 or 3rd Quarter.
|
||||
// - 'zzzz' for full name of timezone like "Japan Standard Time" or "Eastern Standard Time".
|
||||
// - 'O' for localized zone-offset, like GMT+8 or GMT+08:00.
|
||||
// - '[]' for optional section of the format.
|
||||
// - 'G' for era, like AD or BC.
|
||||
// - 'W' for week of month.
|
||||
// - 'D' for day of year.
|
||||
// So date strings with these date elements cannot be parsed.
|
||||
var timeElementToLayout = map[byte]map[int]string{
|
||||
'M': {
|
||||
4: "January", // MMMM = full month name
|
||||
3: "Jan", // MMM = short month name
|
||||
2: "01", // MM = month of year (2 digits) (01-12)
|
||||
1: "1", // M = month of year (1 digit) (1-12)
|
||||
},
|
||||
'd': {
|
||||
2: "02", // dd = day of month (2 digits) (01-31)
|
||||
1: "2", // d = day of month (1 digit) (1-31)
|
||||
},
|
||||
'a': {
|
||||
2: "pm", // aa = pm/am
|
||||
1: "PM", // a = PM/AM
|
||||
},
|
||||
'H': {
|
||||
2: "15", // HH = hour (24 hour clock) (2 digits)
|
||||
1: "15", // H = hour (24 hour clock) (1 digit)
|
||||
},
|
||||
'm': {
|
||||
2: "04", // mm = minute (2 digits)
|
||||
1: "4", // m = minute (1 digit)
|
||||
},
|
||||
's': {
|
||||
2: "05", // ss = seconds (2 digits)
|
||||
1: "5", // s = seconds (1 digit)
|
||||
},
|
||||
|
||||
// timezone offsets from UTC below
|
||||
'X': {
|
||||
5: "Z07:00:00", // XXXXX = timezone offset (+-hh:mm:ss)
|
||||
4: "Z070000", // XXXX = timezone offset (+-hhmmss)
|
||||
3: "Z07:00", // XXX = timezone offset (+-hh:mm)
|
||||
2: "Z0700", // XX = timezone offset (+-hhmm)
|
||||
1: "Z07", // X = timezone offset (+-hh)
|
||||
},
|
||||
'x': {
|
||||
5: "-07:00:00", // xxxxx = timezone offset (+-hh:mm:ss)
|
||||
4: "-070000", // xxxx = timezone offset (+-hhmmss)
|
||||
3: "-07:00", // xxx = timezone offset (+-hh:mm)
|
||||
2: "-0700", // xx = timezone offset (+-hhmm)
|
||||
1: "-07", // x = timezone offset (+-hh)
|
||||
},
|
||||
}
|
||||
|
||||
type DateTimeParser struct {
|
||||
layouts []string
|
||||
}
|
||||
|
||||
func New(layouts []string) *DateTimeParser {
|
||||
return &DateTimeParser{
|
||||
layouts: layouts,
|
||||
}
|
||||
}
|
||||
|
||||
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 letterCounter(layout string, idx int) int {
|
||||
count := 1
|
||||
for idx+count < len(layout) {
|
||||
if layout[idx+count] == layout[idx] {
|
||||
count++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func invalidFormatError(character byte, count int) error {
|
||||
return fmt.Errorf("invalid format string, unknown format specifier: %s", strings.Repeat(string(character), count))
|
||||
}
|
||||
|
||||
func parseISOString(layout string) (string, error) {
|
||||
var dateTimeLayout strings.Builder
|
||||
|
||||
for idx := 0; idx < len(layout); {
|
||||
// check if the character is a text literal delimiter (')
|
||||
if layout[idx] == textLiteralDelimiter {
|
||||
if idx+1 < len(layout) && layout[idx+1] == textLiteralDelimiter {
|
||||
// if the next character is also a text literal delimiter, then
|
||||
// copy the character as is
|
||||
dateTimeLayout.WriteByte(textLiteralDelimiter)
|
||||
idx += 2
|
||||
continue
|
||||
}
|
||||
// find the next text literal delimiter
|
||||
for idx++; idx < len(layout); idx++ {
|
||||
if layout[idx] == textLiteralDelimiter {
|
||||
break
|
||||
}
|
||||
dateTimeLayout.WriteByte(layout[idx])
|
||||
}
|
||||
// idx can either be equal to len(layout) if the text literal delimiter is not found
|
||||
// after the first text literal delimiter or it will be equal to the index of the
|
||||
// second text literal delimiter
|
||||
if idx == len(layout) {
|
||||
// text literal delimiter not found error
|
||||
return "", fmt.Errorf("invalid format string, expected text literal delimiter: %s", string(textLiteralDelimiter))
|
||||
}
|
||||
// increment idx to skip the second text literal delimiter
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
// check if character is a letter in english alphabet - a-zA-Z which are reserved
|
||||
// for format specifiers
|
||||
if (layout[idx] >= 'a' && layout[idx] <= 'z') || (layout[idx] >= 'A' && layout[idx] <= 'Z') {
|
||||
// find the number of times the character occurs consecutively
|
||||
count := letterCounter(layout, idx)
|
||||
character := layout[idx]
|
||||
// first check the table
|
||||
if layout, ok := timeElementToLayout[character][count]; ok {
|
||||
dateTimeLayout.WriteString(layout)
|
||||
} else {
|
||||
switch character {
|
||||
case 'y', 'u', 'Y':
|
||||
// year
|
||||
if count == 2 {
|
||||
dateTimeLayout.WriteString("06")
|
||||
} else {
|
||||
format := fmt.Sprintf("%%0%ds", count)
|
||||
dateTimeLayout.WriteString(fmt.Sprintf(format, "2006"))
|
||||
}
|
||||
case 'h', 'K':
|
||||
// hour (1-12)
|
||||
switch count {
|
||||
case 2:
|
||||
// hh, KK -> 03
|
||||
dateTimeLayout.WriteString("03")
|
||||
case 1:
|
||||
// h, K -> 3
|
||||
dateTimeLayout.WriteString("3")
|
||||
default:
|
||||
// e.g., hhh
|
||||
return "", invalidFormatError(character, count)
|
||||
}
|
||||
case 'E':
|
||||
// day of week
|
||||
if count == 4 {
|
||||
dateTimeLayout.WriteString("Monday") // EEEE -> Monday
|
||||
} else if count <= 3 {
|
||||
dateTimeLayout.WriteString("Mon") // E, EE, EEE -> Mon
|
||||
} else {
|
||||
return "", invalidFormatError(character, count) // e.g., EEEEE
|
||||
}
|
||||
case 'S':
|
||||
// fraction of second
|
||||
// .SSS = millisecond
|
||||
// .SSSSSS = microsecond
|
||||
// .SSSSSSSSS = nanosecond
|
||||
if count > 9 {
|
||||
return "", invalidFormatError(character, count)
|
||||
}
|
||||
dateTimeLayout.WriteString(strings.Repeat(string('0'), count))
|
||||
case 'z':
|
||||
// timezone id
|
||||
if count < 5 {
|
||||
dateTimeLayout.WriteString("MST")
|
||||
} else {
|
||||
return "", invalidFormatError(character, count)
|
||||
}
|
||||
default:
|
||||
return "", invalidFormatError(character, count)
|
||||
}
|
||||
}
|
||||
idx += count
|
||||
} else {
|
||||
// copy the character as is
|
||||
dateTimeLayout.WriteByte(layout[idx])
|
||||
idx++
|
||||
}
|
||||
}
|
||||
return dateTimeLayout.String(), nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
var layoutStrs []string
|
||||
for _, layout := range layouts {
|
||||
layoutStr, ok := layout.(string)
|
||||
if ok {
|
||||
layout, err := parseISOString(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)
|
||||
}
|
||||
}
|
||||
182
analysis/datetime/iso/iso_test.go
Normal file
182
analysis/datetime/iso/iso_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
// 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 iso
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConversionFromISOStyle(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
output string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
input: "yyyy-MM-dd",
|
||||
output: "2006-01-02",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "uuu/M''''dd'T'HH:m:ss.SSS",
|
||||
output: "2006/1''02T15:4:05.000",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "YYYY-MM-dd'T'H:mm:ss zzz",
|
||||
output: "2006-01-02T15:04:05 MST",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "MMMM dd yyyy', 'HH:mm:ss.SSS",
|
||||
output: "January 02 2006, 15:04:05.000",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "h 'o'''' clock' a, XXX",
|
||||
output: "3 o' clock PM, Z07:00",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "YYYY-MM-dd'T'HH:mm:ss'Z'",
|
||||
output: "2006-01-02T15:04:05Z",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "E MMM d H:mm:ss z Y",
|
||||
output: "Mon Jan 2 15:04:05 MST 2006",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "E MMM DD H:m:s z Y",
|
||||
output: "",
|
||||
err: fmt.Errorf("invalid format string, unknown format specifier: DD"),
|
||||
},
|
||||
{
|
||||
input: "E MMM''''' H:m:s z Y",
|
||||
output: "",
|
||||
err: fmt.Errorf("invalid format string, expected text literal delimiter: '"),
|
||||
},
|
||||
{
|
||||
input: "MMMMM dd yyyy', 'HH:mm:ss.SSS",
|
||||
output: "",
|
||||
err: fmt.Errorf("invalid format string, unknown format specifier: MMMMM"),
|
||||
},
|
||||
{
|
||||
input: "yy", // year (2 digits)
|
||||
output: "06",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "yyyyy", // year (5 digits, padded)
|
||||
output: "02006",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "h", // hour 1-12 (1 digit)
|
||||
output: "3",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "hh", // hour 1-12 (2 digits)
|
||||
output: "03",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "KK", // hour 1-12 (2 digits, alt)
|
||||
output: "03",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "hhh", // invalid hour count
|
||||
output: "",
|
||||
err: fmt.Errorf("invalid format string, unknown format specifier: hhh"),
|
||||
},
|
||||
{
|
||||
input: "E", // Day of week (short)
|
||||
output: "Mon",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "EEE", // Day of week (short)
|
||||
output: "Mon",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "EEEE", // Day of week (long)
|
||||
output: "Monday",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "EEEEE", // Day of week (long)
|
||||
output: "",
|
||||
err: fmt.Errorf("invalid format string, unknown format specifier: EEEEE"),
|
||||
},
|
||||
{
|
||||
input: "S", // Fraction of second (1 digit)
|
||||
output: "0",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "SSSSSSSSS", // Fraction of second (9 digits)
|
||||
output: "000000000",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "SSSSSSSSSS", // Invalid fraction of second count
|
||||
output: "",
|
||||
err: fmt.Errorf("invalid format string, unknown format specifier: SSSSSSSSSS"),
|
||||
},
|
||||
{
|
||||
input: "z", // Timezone name (short)
|
||||
output: "MST",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "zzz", // Timezone name (short) - Corrected expectation
|
||||
output: "MST", // Should output MST
|
||||
err: nil, // Should not produce an error
|
||||
},
|
||||
{
|
||||
input: "zzzz", // Timezone name (long) - Corrected expectation
|
||||
output: "MST", // Should output MST
|
||||
err: nil, // Should not produce an error
|
||||
},
|
||||
{
|
||||
input: "G", // Era designator (unsupported)
|
||||
output: "",
|
||||
err: fmt.Errorf("invalid format string, unknown format specifier: G"),
|
||||
},
|
||||
{
|
||||
input: "W", // Week of month (unsupported)
|
||||
output: "",
|
||||
err: fmt.Errorf("invalid format string, unknown format specifier: W"),
|
||||
},
|
||||
}
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("test %d: %s", i, test.input), func(t *testing.T) {
|
||||
out, err := parseISOString(test.input)
|
||||
// Check error matching
|
||||
if (err != nil && test.err == nil) || (err == nil && test.err != nil) || (err != nil && test.err != nil && err.Error() != test.err.Error()) {
|
||||
t.Fatalf("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("expected output '%v', got '%v'", test.output, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
analysis/datetime/optional/optional.go
Normal file
50
analysis/datetime/optional/optional.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2014 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 optional
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/analysis/datetime/flexible"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
)
|
||||
|
||||
const Name = "dateTimeOptional"
|
||||
|
||||
const rfc3339NoTimezone = "2006-01-02T15:04:05"
|
||||
const rfc3339NoTimezoneNoT = "2006-01-02 15:04:05"
|
||||
const rfc3339Offset = "2006-01-02 15:04:05 -0700"
|
||||
const rfc3339NoTime = "2006-01-02"
|
||||
|
||||
var layouts = []string{
|
||||
time.RFC3339Nano,
|
||||
time.RFC3339,
|
||||
rfc3339NoTimezone,
|
||||
rfc3339NoTimezoneNoT,
|
||||
rfc3339Offset,
|
||||
rfc3339NoTime,
|
||||
}
|
||||
|
||||
func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
|
||||
return flexible.New(layouts), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
205
analysis/datetime/percent/percent.go
Normal file
205
analysis/datetime/percent/percent.go
Normal 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)
|
||||
}
|
||||
}
|
||||
474
analysis/datetime/percent/percent_test.go
Normal file
474
analysis/datetime/percent/percent_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
130
analysis/datetime/sanitized/sanitized.go
Normal file
130
analysis/datetime/sanitized/sanitized.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// 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 sanitized
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
)
|
||||
|
||||
const Name = "sanitizedgo"
|
||||
|
||||
var validMagicNumbers = map[string]struct{}{
|
||||
"2006": {},
|
||||
"06": {}, // Year
|
||||
"01": {},
|
||||
"1": {},
|
||||
"_1": {},
|
||||
"January": {},
|
||||
"Jan": {}, // Month
|
||||
"02": {},
|
||||
"2": {},
|
||||
"_2": {},
|
||||
"__2": {},
|
||||
"002": {},
|
||||
"Monday": {},
|
||||
"Mon": {}, // Day
|
||||
"15": {},
|
||||
"3": {},
|
||||
"03": {}, // Hour
|
||||
"4": {},
|
||||
"04": {}, // Minute
|
||||
"5": {},
|
||||
"05": {}, // Second
|
||||
"0700": {},
|
||||
"070000": {},
|
||||
"07": {},
|
||||
"00": {},
|
||||
"": {},
|
||||
}
|
||||
|
||||
var layoutSplitRegex = regexp.MustCompile("[\\+\\-= :T,Z\\.<>;\\?!`~@#$%\\^&\\*|'\"\\(\\){}\\[\\]/\\\\]")
|
||||
|
||||
var layoutStripRegex = regexp.MustCompile(`PM|pm|\.9+|\.0+|MST`)
|
||||
|
||||
type DateTimeParser struct {
|
||||
layouts []string
|
||||
}
|
||||
|
||||
func New(layouts []string) *DateTimeParser {
|
||||
return &DateTimeParser{
|
||||
layouts: layouts,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// date time layouts must be a combination of constants specified in golang time package
|
||||
// https://pkg.go.dev/time#pkg-constants
|
||||
// this validation verifies that only these constants are used in the custom layout
|
||||
// for compatibility with the golang time package
|
||||
func validateLayout(layout string) bool {
|
||||
// first we strip out commonly used constants
|
||||
// such as "PM" which can be present in the layout
|
||||
// right after a time component, e.g. 03:04PM;
|
||||
// because regex split cannot separate "03:04PM" into
|
||||
// "03:04" and "PM". We also strip out ".9+" and ".0+"
|
||||
// which represent fractional seconds.
|
||||
layout = layoutStripRegex.ReplaceAllString(layout, "")
|
||||
// then we split the layout by non-constant characters
|
||||
// which is a regex and verify that each split is a valid magic number
|
||||
split := layoutSplitRegex.Split(layout, -1)
|
||||
for i := range split {
|
||||
_, found := validMagicNumbers[split[i]]
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
var layoutStrs []string
|
||||
for _, layout := range layouts {
|
||||
layoutStr, ok := layout.(string)
|
||||
if ok {
|
||||
if !validateLayout(layoutStr) {
|
||||
return nil, fmt.Errorf("invalid datetime parser layout: %s,"+
|
||||
" please refer to https://pkg.go.dev/time#pkg-constants for supported"+
|
||||
" layouts", layoutStr)
|
||||
}
|
||||
layoutStrs = append(layoutStrs, layoutStr)
|
||||
}
|
||||
}
|
||||
return New(layoutStrs), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
109
analysis/datetime/sanitized/sanitized_test.go
Normal file
109
analysis/datetime/sanitized/sanitized_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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 sanitized
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLayoutValidatorRegex(t *testing.T) {
|
||||
splitRegexTests := []struct {
|
||||
input string
|
||||
output []string
|
||||
}{
|
||||
{
|
||||
input: "2014-08-03",
|
||||
output: []string{"2014", "08", "03"},
|
||||
},
|
||||
{
|
||||
input: "2014-08-03T15:59:30",
|
||||
output: []string{"2014", "08", "03", "15", "59", "30"},
|
||||
},
|
||||
{
|
||||
input: "2014.08-03 15/59`30",
|
||||
output: []string{"2014", "08", "03", "15", "59", "30"},
|
||||
},
|
||||
{
|
||||
input: "2014/08/03T15:59:30Z08:00",
|
||||
output: []string{"2014", "08", "03", "15", "59", "30", "08", "00"},
|
||||
},
|
||||
{
|
||||
input: "2014\\08|03T15=59.30.999999999+08*00",
|
||||
output: []string{"2014", "08", "03", "15", "59", "30", "999999999", "08", "00"},
|
||||
},
|
||||
{
|
||||
input: "2006-01-02T15:04:05.999999999Z07:00",
|
||||
output: []string{"2006", "01", "02", "15", "04", "05", "999999999", "07", "00"},
|
||||
},
|
||||
{
|
||||
input: "A-B C:DTE,FZG.H<I>J;K?L!M`N~O@P#Q$R%S^U&V*W|X'Y\"A(B)C{D}E[F]G/H\\I+J=L",
|
||||
output: []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P",
|
||||
"Q", "R", "S", "U", "V", "W", "X", "Y", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "L"},
|
||||
},
|
||||
}
|
||||
regex := layoutSplitRegex
|
||||
for _, test := range splitRegexTests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
actualOutput := regex.Split(test.input, -1)
|
||||
if !reflect.DeepEqual(actualOutput, test.output) {
|
||||
t.Fatalf("expected output %v, got %v", test.output, actualOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stripRegexTests := []struct {
|
||||
input string
|
||||
output string
|
||||
}{
|
||||
{
|
||||
input: "3PM",
|
||||
output: "3",
|
||||
},
|
||||
{
|
||||
input: "3.0PM",
|
||||
output: "3",
|
||||
},
|
||||
{
|
||||
input: "3.9AM",
|
||||
output: "3AM",
|
||||
},
|
||||
{
|
||||
input: "3.999999999pm",
|
||||
output: "3",
|
||||
},
|
||||
{
|
||||
input: "2006-01-02T15:04:05.999999999Z07:00MST",
|
||||
output: "2006-01-02T15:04:05Z07:00",
|
||||
},
|
||||
{
|
||||
input: "Jan _2 15:04:05.0000000+07:00MST",
|
||||
output: "Jan _2 15:04:05+07:00",
|
||||
},
|
||||
{
|
||||
input: "15:04:05.99PM+07:00MST",
|
||||
output: "15:04:05+07:00",
|
||||
},
|
||||
}
|
||||
regex = layoutStripRegex
|
||||
for _, test := range stripRegexTests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
actualOutput := layoutStripRegex.ReplaceAllString(test.input, "")
|
||||
if !reflect.DeepEqual(actualOutput, test.output) {
|
||||
t.Fatalf("expected output %v, got %v", test.output, actualOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
analysis/datetime/timestamp/microseconds/microseconds.go
Normal file
55
analysis/datetime/timestamp/microseconds/microseconds.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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 microseconds
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
)
|
||||
|
||||
const Name = "unix_micro"
|
||||
|
||||
type DateTimeParser struct {
|
||||
}
|
||||
|
||||
var minBound int64 = math.MinInt64 / 1000
|
||||
var maxBound int64 = math.MaxInt64 / 1000
|
||||
|
||||
func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) {
|
||||
// unix timestamp is milliseconds since UNIX epoch
|
||||
timestamp, err := strconv.ParseInt(input, 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, "", analysis.ErrInvalidTimestampString
|
||||
}
|
||||
if timestamp < minBound || timestamp > maxBound {
|
||||
return time.Time{}, "", analysis.ErrInvalidTimestampRange
|
||||
}
|
||||
return time.UnixMicro(timestamp), Name, nil
|
||||
}
|
||||
|
||||
func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
|
||||
return &DateTimeParser{}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
55
analysis/datetime/timestamp/milliseconds/milliseconds.go
Normal file
55
analysis/datetime/timestamp/milliseconds/milliseconds.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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 milliseconds
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
)
|
||||
|
||||
const Name = "unix_milli"
|
||||
|
||||
type DateTimeParser struct {
|
||||
}
|
||||
|
||||
var minBound int64 = math.MinInt64 / 1000000
|
||||
var maxBound int64 = math.MaxInt64 / 1000000
|
||||
|
||||
func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) {
|
||||
// unix timestamp is milliseconds since UNIX epoch
|
||||
timestamp, err := strconv.ParseInt(input, 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, "", analysis.ErrInvalidTimestampString
|
||||
}
|
||||
if timestamp < minBound || timestamp > maxBound {
|
||||
return time.Time{}, "", analysis.ErrInvalidTimestampRange
|
||||
}
|
||||
return time.UnixMilli(timestamp), Name, nil
|
||||
}
|
||||
|
||||
func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
|
||||
return &DateTimeParser{}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
55
analysis/datetime/timestamp/nanoseconds/nanoseconds.go
Normal file
55
analysis/datetime/timestamp/nanoseconds/nanoseconds.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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 nanoseconds
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
)
|
||||
|
||||
const Name = "unix_nano"
|
||||
|
||||
type DateTimeParser struct {
|
||||
}
|
||||
|
||||
var minBound int64 = math.MinInt64
|
||||
var maxBound int64 = math.MaxInt64
|
||||
|
||||
func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) {
|
||||
// unix timestamp is milliseconds since UNIX epoch
|
||||
timestamp, err := strconv.ParseInt(input, 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, "", analysis.ErrInvalidTimestampString
|
||||
}
|
||||
if timestamp < minBound || timestamp > maxBound {
|
||||
return time.Time{}, "", analysis.ErrInvalidTimestampRange
|
||||
}
|
||||
return time.Unix(0, timestamp), Name, nil
|
||||
}
|
||||
|
||||
func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
|
||||
return &DateTimeParser{}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
55
analysis/datetime/timestamp/seconds/seconds.go
Normal file
55
analysis/datetime/timestamp/seconds/seconds.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2014 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 seconds
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve/v2/analysis"
|
||||
"github.com/blevesearch/bleve/v2/registry"
|
||||
)
|
||||
|
||||
const Name = "unix_sec"
|
||||
|
||||
type DateTimeParser struct {
|
||||
}
|
||||
|
||||
var minBound int64 = math.MinInt64 / 1000000000
|
||||
var maxBound int64 = math.MaxInt64 / 1000000000
|
||||
|
||||
func (p *DateTimeParser) ParseDateTime(input string) (time.Time, string, error) {
|
||||
// unix timestamp is seconds since UNIX epoch
|
||||
timestamp, err := strconv.ParseInt(input, 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, "", analysis.ErrInvalidTimestampString
|
||||
}
|
||||
if timestamp < minBound || timestamp > maxBound {
|
||||
return time.Time{}, "", analysis.ErrInvalidTimestampRange
|
||||
}
|
||||
return time.Unix(timestamp, 0), Name, nil
|
||||
}
|
||||
|
||||
func DateTimeParserConstructor(config map[string]interface{}, cache *registry.Cache) (analysis.DateTimeParser, error) {
|
||||
return &DateTimeParser{}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
err := registry.RegisterDateTimeParser(Name, DateTimeParserConstructor)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user