diff --git a/mapper.go b/mapper.go index 0523f11..184de29 100644 --- a/mapper.go +++ b/mapper.go @@ -2,6 +2,7 @@ package tupleconv import ( "encoding/json" + "errors" "fmt" "github.com/google/uuid" "github.com/tarantool/go-tarantool/v2/datetime" @@ -30,6 +31,7 @@ var ( _ Mapper[string, any] = (*StringToSliceMapper)(nil) _ Mapper[string, any] = (*StringToNullMapper)(nil) _ Mapper[string, any] = (*IdentityMapper[string])(nil) + _ Mapper[string, any] = (*StringToIntervalMapper)(nil) ) // IdentityMapper is a mapper from S to any, that doesn't change the input. @@ -260,3 +262,48 @@ func (mapper StringToNullMapper) Map(src string) (any, error) { } return nil, nil } + +// StringToIntervalMapper is a mapper from string to datetime.Interval. +type StringToIntervalMapper struct{} + +// MakeStringToIntervalMapper creates StringToIntervalMapper. +func MakeStringToIntervalMapper() StringToIntervalMapper { + return StringToIntervalMapper{} +} + +// intervalFieldsNumber is the number of fields in datetime.Interval. +const intervalFieldsNumber = 9 + +var errUnexpectedIntervalFmt = errors.New("unexpected interval format") + +// Map is the implementation of Mapper[string] for StringToIntervalMapper. +func (StringToIntervalMapper) Map(src string) (any, error) { + parts := strings.Split(src, ",") + if len(parts) != intervalFieldsNumber { + return nil, errUnexpectedIntervalFmt + } + partsAsInt64 := [intervalFieldsNumber]int64{} + for i, part := range parts { + var err error + if partsAsInt64[i], err = strconv.ParseInt(part, 10, 64); err != nil { + return nil, errUnexpectedIntervalFmt + } + } + adjust := datetime.Adjust(partsAsInt64[8]) + if adjust != datetime.NoneAdjust && + adjust != datetime.ExcessAdjust && adjust != datetime.LastAdjust { + return nil, errUnexpectedIntervalFmt + } + interval := datetime.Interval{ + Year: partsAsInt64[0], + Month: partsAsInt64[1], + Week: partsAsInt64[2], + Day: partsAsInt64[3], + Hour: partsAsInt64[4], + Min: partsAsInt64[5], + Sec: partsAsInt64[6], + Nsec: partsAsInt64[7], + Adjust: adjust, + } + return interval, nil +} diff --git a/mapper_test.go b/mapper_test.go index 785bb96..13728a7 100644 --- a/mapper_test.go +++ b/mapper_test.go @@ -262,6 +262,27 @@ func TestParsers(t *testing.T) { {value: "-1/0", isErr: true}, {value: "1**5", isErr: true}, }, + tupleconv.MakeStringToIntervalMapper(): { + // Basic. + { + value: "1,2,3,4,5,6,7,8,1", + expected: datetime.Interval{ + Year: 1, Month: 2, Week: 3, Day: 4, Hour: 5, Min: 6, Sec: 7, Nsec: 8, Adjust: 1, + }, + }, + { + value: "-11,-1110,0,0,0,0,0,1,2", + expected: datetime.Interval{ + Year: -11, Month: -1110, Nsec: 1, Adjust: 2, + }, + }, + + // Error. + {value: "1,2,3,4,5,6,7,8,3", isErr: true}, // Invalid adjust. + {value: "1,2,3", isErr: true}, + {value: "1,2,3,4,5,6,7a,8,0", isErr: true}, + {value: "", isErr: true}, + }, } for parser, cases := range tests { diff --git a/tt.go b/tt.go index 2efda76..1d2cd4a 100644 --- a/tt.go +++ b/tt.go @@ -24,6 +24,7 @@ const ( TypeVarbinary TypeName = "varbinary" TypeScalar TypeName = "scalar" TypeAny TypeName = "any" + TypeInterval TypeName = "interval" ) const ( @@ -76,6 +77,9 @@ type TypeToTTMapperFactory[Type any] interface { // MakeTypeToScalarMapper creates a mapper from Type to scalar. MakeTypeToScalarMapper() Mapper[Type, any] + // MakeTypeToIntervalMapper creates a mapper from Type to interval. + MakeTypeToIntervalMapper() Mapper[Type, any] + // MakeNullableMapper extends the incoming mapper to a nullable mapper. MakeNullableMapper(Mapper[Type, any]) Mapper[Type, any] } @@ -184,6 +188,7 @@ func (fac *StringToTTMapperFactory) MakeTypeToAnyMapper() Mapper[string, any] { fac.MakeTypeToBooleanMapper(), fac.MakeTypeToDatetimeMapper(), fac.MakeTypeToUUIDMapper(), + fac.MakeTypeToIntervalMapper(), fac.MakeTypeToStringMapper(), }) } @@ -195,6 +200,7 @@ func (fac *StringToTTMapperFactory) MakeTypeToScalarMapper() Mapper[string, any] fac.MakeTypeToBooleanMapper(), fac.MakeTypeToDatetimeMapper(), fac.MakeTypeToUUIDMapper(), + fac.MakeTypeToIntervalMapper(), fac.MakeTypeToStringMapper(), }) } @@ -207,6 +213,10 @@ func (fac *StringToTTMapperFactory) MakeNullableMapper( }) } +func (fac *StringToTTMapperFactory) MakeTypeToIntervalMapper() Mapper[string, any] { + return MakeStringToIntervalMapper() +} + // Cfg configures StringToTTMapperFactory with the provided options. func (fac *StringToTTMapperFactory) Cfg(opts StringToTTMapperOpts) { fac.opts = opts @@ -251,6 +261,8 @@ func MakeTypeToTTMapperList[Type any]( mappers[i] = fac.MakeTypeToAnyMapper() case TypeScalar: mappers[i] = fac.MakeTypeToScalarMapper() + case TypeInterval: + mappers[i] = fac.MakeTypeToIntervalMapper() default: return nil, fmt.Errorf("unexpected type: %s", typ) } diff --git a/tt_test.go b/tt_test.go index 44a569b..e3efe29 100644 --- a/tt_test.go +++ b/tt_test.go @@ -297,6 +297,18 @@ func TestMakeStringTOTTMapperList_defaultFactory(t *testing.T) { {value: "09b56913-11f0-4fa4-b5d0-901b5efa532a", expected: someUUID}, {value: "2020-08-22T11:27:43.123456789-02:00", expected: dateTime1}, {value: "1`e2", expected: "1`e2"}, + { + value: "1,2,3,4,5,6,7,8,1", + expected: datetime.Interval{ + Year: 1, Month: 2, Week: 3, Day: 4, Hour: 5, Min: 6, Sec: 7, Nsec: 8, Adjust: 1, + }, + }, + { + value: "-11,-1110,0,0,0,0,0,1,2", + expected: datetime.Interval{ + Year: -11, Month: -1110, Nsec: 1, Adjust: 2, + }, + }, }, tupleconv.TypeScalar: { {value: "blablabla", expected: "blablabla"}, @@ -310,6 +322,18 @@ func TestMakeStringTOTTMapperList_defaultFactory(t *testing.T) { {value: "09b56913-11f0-4fa4-b5d0-901b5efa532a", expected: someUUID}, {value: "2020-08-22T11:27:43.123456789-02:00", expected: dateTime1}, {value: "1`e2", expected: "1`e2"}, + { + value: "1,2,3,4,5,6,7,8,1", + expected: datetime.Interval{ + Year: 1, Month: 2, Week: 3, Day: 4, Hour: 5, Min: 6, Sec: 7, Nsec: 8, Adjust: 1, + }, + }, + { + value: "-11,-1110,0,0,0,0,0,1,2", + expected: datetime.Interval{ + Year: -11, Month: -1110, Nsec: 1, Adjust: 2, + }, + }, }, tupleconv.TypeDecimal: { @@ -350,6 +374,30 @@ func TestMakeStringTOTTMapperList_defaultFactory(t *testing.T) { {value: "-1/0", isErr: true}, {value: "1**5", isErr: true}, }, + tupleconv.TypeInterval: { + // Basic. + { + value: "1,2,3,4,5,6,7,8,1", + expected: datetime.Interval{ + Year: 1, Month: 2, Week: 3, Day: 4, Hour: 5, Min: 6, Sec: 7, Nsec: 8, Adjust: 1, + }, + }, + { + value: "-11,-1110,0,0,0,0,0,1,2", + expected: datetime.Interval{ + Year: -11, Month: -1110, Nsec: 1, Adjust: 2, + }, + }, + + // Nullable. + {value: "", isNullable: true, expected: nil}, + + // Error. + {value: "1,2,3,4,5,6,7,8,3", isErr: true}, // Invalid adjust. + {value: "1,2,3", isErr: true}, + {value: "1,2,3,4,5,6,7a,8,0", isErr: true}, + {value: "", isErr: true}, + }, } fac := tupleconv.NewStringToTTMapperFactory() @@ -589,6 +637,31 @@ func TestMakeStringTOTTMapperList_configuredFactory(t *testing.T) { {value: "1,,2", isErr: true}, {value: "hello", isErr: true}, }, + tupleconv.TypeInterval: { + // Basic. + { + value: "1,2,3,4,5,6,7,8,1", + expected: datetime.Interval{ + Year: 1, Month: 2, Week: 3, Day: 4, Hour: 5, Min: 6, Sec: 7, Nsec: 8, Adjust: 1, + }, + }, + { + value: "-11,-1110,0,0,0,0,0,1,2", + expected: datetime.Interval{ + Year: -11, Month: -1110, Nsec: 1, Adjust: 2, + }, + }, + + // Nullable. + {value: "null", isNullable: true, expected: nil}, + {value: "", isNullable: true, isErr: true}, + + // Error. + {value: "1#2#3#4#5#6#7#8#3", isErr: true}, // Invalid adjust. + {value: "1,2,3", isErr: true}, + {value: "1,2,3,4,5,6,7a,8,0", isErr: true}, + {value: "", isErr: true}, + }, } opts := tupleconv.StringToTTMapperOpts{