├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── Untitled.png ├── context.go ├── context_builder.go ├── float_formatter.go ├── format.go ├── format_test.go ├── formatter.go ├── go.mod ├── go.sum ├── int_formatter.go ├── nope_formatter.go ├── split.go ├── split_test.go ├── string_formatter.go ├── time_formatter.go └── value_formatter.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.9.x 5 | 6 | install: 7 | - go get -v -d -t github.com/sirkon/go-format 8 | 9 | script: 10 | - make test 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Denis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | PATH=${GOPATH}/bin:${PATH} 3 | go get -u github.com/stretchr/testify 4 | go test -test.v github.com/sirkon/go-format 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-format 2 | [![Build Status](https://travis-ci.org/sirkon/go-format.svg?branch=master)](https://travis-ci.org/sirkon/go-format) 3 | 4 | Simple string formatting tool with date arithmetics and format (strftime) support. Be prepared though: shortcut functions like `format.Fortmatp` are aimed to be used in my code generations tools where the correct formatting matters, thus they are just panics on incorrect calls, such as parameters and formatting mismatch and this works really good for them. 5 | 6 | ### Installation 7 | ```bash 8 | GO111MODULE=on go get github.com/sirkon/go-format/v2 9 | ``` 10 | 11 | ### Rationale behind 12 | In analytical systems you need to deal with time intervals. This formatter helps to achieve this right at the configuration 13 | file level. 14 | 15 | ### Examples 16 | 17 | ##### How to escape $ 18 | ```go 19 | res := format.Formatp("$$0 $0", 1) 20 | // rest = "$0 1" 21 | ``` 22 | 23 | ##### Positional parameters, you can use `${1}`, `${2}` to address specific parameter (by number) 24 | ```go 25 | res := format.Formatp("$ ${} $1 ${0}", 1, 2) 26 | // res = "1 2 2 1" 27 | ``` 28 | 29 | 30 | ##### Named parameters via map[string]interface{} 31 | ```go 32 | res := format.Formatm("${name} $count ${weight|1.2}", format.Values{ 33 | "name": "name", 34 | "count": 12, 35 | "weight": 0.79, 36 | }) 37 | // res = "name 12 0.79" 38 | ``` 39 | 40 | ##### Named parameters via type guesses 41 | ```go 42 | var s struct { 43 | A string 44 | Field int 45 | } 46 | s.A = "str" 47 | s.Field = 12 48 | res := format.Formatg("$A $Field", s) 49 | ``` 50 | Substructures are supported. Also, any kind of map with keys being one of 51 | 1) string 52 | 2) any kind of integer, signed or unsigned 53 | 3) `fmt.Stringer` 54 | is supported as well. 55 | 56 | ```go 57 | type t struct { 58 | A string 59 | Field int 60 | } 61 | var s = t{ 62 | A: "str", 63 | Field: 12, 64 | } 65 | var d struct { 66 | F t 67 | Entry float64 68 | } 69 | d.F = s 70 | d.Entry = 0.5 71 | res := format.Formatg("${F.A} ${F.Field} $Entry", d) 72 | // res = "str 12 0.500000" 73 | ``` 74 | 75 | ```go 76 | v := map[int]string{ 77 | 1: "bc", 78 | 12: "bd" 79 | } 80 | res := format.Formatg("$1-$12") 81 | // res = "bc-bc" 82 | ``` 83 | 84 | ##### Use given `func(string) string` function as a source of values 85 | 86 | It is possible to use function like `os.Getenv` as a source of values. Use `format.Formatf` function for this: 87 | 88 | 89 | ```go 90 | res := format.Formatf("${HOSTTYPE} ${COLUMNS}", os.Getenv) 91 | // res = "x86_64 80" 92 | ``` 93 | Check: 94 | 95 | ![pic](Untitled.png) 96 | 97 | Of course, you can use every `func(string) string` function, not just `os.Getenv` 98 | 99 | ##### Date arithmetics 100 | ```go 101 | t := time.Date(2018, 1, 18, 22, 57, 37, 12, time.UTC) 102 | res := format.Formatm("${ date + 1 day | %Y-%m-%d %H:%M:%S }", format.Values{ 103 | "date": t, 104 | }) 105 | // res = "2018-01-19 22:57:37" 106 | ``` 107 | Date arithmetics allows following expression values: 108 | 109 | * year/years 110 | * month/months 111 | * week/weeks 112 | * day/days 113 | * hour/hours 114 | * minute/minutes 115 | * second/seconds 116 | 117 | Where *year* and *years* (and other couples) are equivalent (*5 years* is nicer than *5 year*, just like *1 year* is prefered over *1 years*). 118 | There's a limitation though, longer period must precedes shorter one, i.e. following expressions are valid. 119 | ``` 120 | + 1 year + 5 weeks + 3 days + 12 seconds 121 | + 25 years + 3 months 122 | - 17 days - 16 hours 123 | + 2 year + 15 weeks + 1 second 124 | ``` 125 | 126 | and this one is invalid 127 | ``` 128 | + 1 week + 5 months 129 | ``` 130 | as a month must precedes a week 131 | 132 | 133 | ##### Low level usage, how it is doing in the background 134 | ```go 135 | package main 136 | 137 | import ( 138 | "fmt" 139 | "time" 140 | 141 | "github.com/sirkon/go-format" 142 | ) 143 | 144 | func main() { 145 | bctx := format.NewContextBuilder() 146 | bctx.AddString("name", "explosion") 147 | bctx.AddInt("count", 15) 148 | bctx.AddTime("time", time.Date(2006, 1, 2, 3, 4, 5, 0, time.UTC)) 149 | ctx, err := bctx.Build() 150 | if err != nil { 151 | panic(err) 152 | } 153 | 154 | res, err := format.Format( 155 | "${name} will be registered by $count independent sources in ${ time + 1 day | %Y-%m-%d } at ${ time | %H:%M:%S }", 156 | ctx) 157 | if err != nil { 158 | panic(err) 159 | } 160 | fmt.Println(res) 161 | } 162 | ``` 163 | 164 | It is probably to be used for most typical usecases. 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirkon/go-format/c96516ac4b519b6f60c3236d7e855adc50f0ec46/Untitled.png -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | // Context ... 10 | type Context interface { 11 | GetFormatter(name string) (Formatter, error) 12 | } 13 | 14 | type contextFromBuilder map[string]Formatter 15 | 16 | func (ctx contextFromBuilder) GetFormatter(name string) (res Formatter, err error) { 17 | res, ok := ctx[name] 18 | if !ok { 19 | formatters := []string{} 20 | for key := range ctx { 21 | formatters = append(formatters, key) 22 | } 23 | sort.Sort(sort.StringSlice(formatters)) 24 | for i, key := range formatters { 25 | formatters[i] = "\t" + key 26 | } 27 | err = fmt.Errorf( 28 | "unknown formatter `%s`, only these are available:\n%s\n", 29 | name, 30 | strings.Join(formatters, "\n"), 31 | ) 32 | } 33 | return 34 | } 35 | 36 | // contextFunc implementation of context using given func as a source of information 37 | type contextFunc func(string) string 38 | 39 | func (f contextFunc) GetFormatter(name string) (Formatter, error) { 40 | value := f(name) 41 | return stringFormatter(value), nil 42 | } 43 | -------------------------------------------------------------------------------- /context_builder.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // ContextBuilder formatting context builder 9 | type ContextBuilder struct { 10 | formatters map[string]Formatter 11 | err error 12 | } 13 | 14 | // NewContextBuilder ... 15 | func NewContextBuilder() *ContextBuilder { 16 | return &ContextBuilder{ 17 | formatters: map[string]Formatter{}, 18 | } 19 | } 20 | 21 | // AddFormatter ... 22 | func (c *ContextBuilder) AddFormatter(name string, formatter Formatter) *ContextBuilder { 23 | if c.err != nil { 24 | return c 25 | } 26 | _, ok := c.formatters[name] 27 | if ok { 28 | c.err = fmt.Errorf("Attemtp to redefine formatter %s", name) 29 | return c 30 | } 31 | c.formatters[name] = formatter 32 | return c 33 | } 34 | 35 | // AddString adds string formatter 36 | func (c *ContextBuilder) AddString(name, value string) *ContextBuilder { 37 | return c.AddFormatter(name, stringFormatter(value)) 38 | } 39 | 40 | // AddInt adds int formatter 41 | func (c *ContextBuilder) AddInt(name string, value int) *ContextBuilder { 42 | return c.AddFormatter(name, intFormatter{ 43 | value: value, 44 | }) 45 | } 46 | 47 | // AddUint adds uint formatter 48 | func (c *ContextBuilder) AddUint(name string, value uint) *ContextBuilder { 49 | return c.AddFormatter(name, uintFormatter{ 50 | value: value, 51 | }) 52 | } 53 | 54 | // AddInt8 adds int8 formatter 55 | func (c *ContextBuilder) AddInt8(name string, value int8) *ContextBuilder { 56 | return c.AddFormatter(name, i8Formatter{ 57 | value: value, 58 | }) 59 | } 60 | 61 | // AddInt16 adds int16 formatter 62 | func (c *ContextBuilder) AddInt16(name string, value int16) *ContextBuilder { 63 | return c.AddFormatter(name, i16Formatter{ 64 | value: value, 65 | }) 66 | } 67 | 68 | // AddInt32 adds int32 formatter 69 | func (c *ContextBuilder) AddInt32(name string, value int32) *ContextBuilder { 70 | return c.AddFormatter(name, i32Formatter{ 71 | value: value, 72 | }) 73 | } 74 | 75 | // AddInt64 adds int64 formatter 76 | func (c *ContextBuilder) AddInt64(name string, value int64) *ContextBuilder { 77 | return c.AddFormatter(name, i64Formatter{ 78 | value: value, 79 | }) 80 | } 81 | 82 | // AddUint8 adds uint8 formatter 83 | func (c *ContextBuilder) AddUint8(name string, value uint8) *ContextBuilder { 84 | return c.AddFormatter(name, u8Formatter{ 85 | value: value, 86 | }) 87 | } 88 | 89 | // AddUint16 adds uint16 formatter 90 | func (c *ContextBuilder) AddUint16(name string, value uint16) *ContextBuilder { 91 | return c.AddFormatter(name, u16Formatter{ 92 | value: value, 93 | }) 94 | } 95 | 96 | // AddUint32 adds uint32 formatter 97 | func (c *ContextBuilder) AddUint32(name string, value uint32) *ContextBuilder { 98 | return c.AddFormatter(name, u32Formatter{ 99 | value: value, 100 | }) 101 | } 102 | 103 | // AddUint64 adds uint64 formatter 104 | func (c *ContextBuilder) AddUint64(name string, value uint64) *ContextBuilder { 105 | return c.AddFormatter(name, u64Formatter{ 106 | value: value, 107 | }) 108 | } 109 | 110 | // AddFloat adds floating point number formatter 111 | func (c *ContextBuilder) AddFloat32(name string, value float32) *ContextBuilder { 112 | return c.AddFormatter(name, f32Formatter{ 113 | value: value, 114 | }) 115 | } 116 | 117 | // AddFloat adds floating point number formatter 118 | func (c *ContextBuilder) AddFloat64(name string, value float64) *ContextBuilder { 119 | return c.AddFormatter(name, f64Formatter{ 120 | value: value, 121 | }) 122 | } 123 | 124 | // AddTime adds time formatter 125 | func (c *ContextBuilder) AddTime(name string, datetime time.Time) *ContextBuilder { 126 | return c.AddFormatter(name, timeFormatter(datetime)) 127 | } 128 | 129 | // AddValue adds formatter to format it as %...v 130 | func (c *ContextBuilder) AddValue(name string, value interface{}) *ContextBuilder { 131 | return c.AddFormatter(name, valueFormatter{ 132 | value: value, 133 | }) 134 | } 135 | 136 | // Add adds formatter with type guessing 137 | func (c *ContextBuilder) Add(name string, value interface{}) *ContextBuilder { 138 | switch v := value.(type) { 139 | case int: 140 | c.AddInt(name, v) 141 | case uint: 142 | c.AddUint(name, v) 143 | case int8: 144 | c.AddInt8(name, v) 145 | case int16: 146 | c.AddInt16(name, v) 147 | case int32: 148 | c.AddInt32(name, v) 149 | case int64: 150 | c.AddInt64(name, v) 151 | case uint8: 152 | c.AddUint8(name, v) 153 | case uint16: 154 | c.AddUint16(name, v) 155 | case uint32: 156 | c.AddUint32(name, v) 157 | case uint64: 158 | c.AddUint64(name, v) 159 | case float32: 160 | c.AddFloat32(name, v) 161 | case float64: 162 | c.AddFloat64(name, v) 163 | case string: 164 | c.AddString(name, v) 165 | case time.Time: 166 | c.AddTime(name, v) 167 | case Formatter: 168 | c.AddFormatter(name, v) 169 | case fmt.Stringer: 170 | c.AddString(name, v.String()) 171 | default: 172 | c.AddValue(name, v) 173 | } 174 | return c 175 | } 176 | 177 | // Build retrieves context object from the builder 178 | func (c *ContextBuilder) Build() (Context, error) { 179 | if c.err != nil { 180 | return nil, c.err 181 | } 182 | return contextFromBuilder(c.formatters), nil 183 | } 184 | -------------------------------------------------------------------------------- /float_formatter.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // f32Formatter ... 9 | type f32Formatter struct { 10 | value float32 11 | } 12 | 13 | // Clarify formatter implementation 14 | func (f f32Formatter) Clarify(a string) (Formatter, error) { 15 | if len(strings.TrimSpace(a)) != 0 { 16 | return f, fmt.Errorf("No clarification available for float32 formatter") 17 | } 18 | return f, nil 19 | } 20 | 21 | // Format formatter implementation 22 | func (f f32Formatter) Format(a string) (string, error) { 23 | return fmt.Sprintf("%"+a+"f", f.value), nil 24 | } 25 | 26 | // f64Formatter ... 27 | type f64Formatter struct { 28 | value float64 29 | } 30 | 31 | // Clarify formatter implementation 32 | func (f f64Formatter) Clarify(a string) (Formatter, error) { 33 | if len(strings.TrimSpace(a)) != 0 { 34 | return f, fmt.Errorf("No clarification available for float64 formatter") 35 | } 36 | return f, nil 37 | } 38 | 39 | // Format formatter implementation 40 | func (f f64Formatter) Format(a string) (string, error) { 41 | return fmt.Sprintf("%"+a+"f", f.value), nil 42 | } 43 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Values is easier than map[string]interface{} 11 | type Values = map[string]interface{} 12 | 13 | // Format function 14 | func Format(format string, context Context) (string, error) { 15 | splitter := NewSplitter(format, context) 16 | var res strings.Builder 17 | for splitter.Split() { 18 | res.WriteString(splitter.Text()) 19 | } 20 | return res.String(), splitter.Err() 21 | } 22 | 23 | // Formatp is a formatting with positional arguments 24 | func Formatp(format string, a ...interface{}) string { 25 | bctx := NewContextBuilder() 26 | for i, value := range a { 27 | key := strconv.Itoa(i) 28 | bctx.Add(key, value) 29 | } 30 | ctx, err := bctx.Build() 31 | if err != nil { 32 | panic(err) 33 | } 34 | res, err := Format(format, ctx) 35 | if err != nil { 36 | panic(err) 37 | } 38 | return res 39 | } 40 | 41 | // Formatm is a formatting with keys from given map 42 | func Formatm(format string, data Values) string { 43 | bctx := NewContextBuilder() 44 | for key, value := range data { 45 | bctx.Add(key, value) 46 | } 47 | ctx, err := bctx.Build() 48 | if err != nil { 49 | panic(err) 50 | } 51 | res, err := Format(format, ctx) 52 | if err != nil { 53 | panic(err) 54 | } 55 | return res 56 | } 57 | 58 | func resolveValue(prefix []string, bctx *ContextBuilder, value reflect.Value) bool { 59 | switch value.Kind() { 60 | case reflect.Ptr: 61 | resolveValue(prefix, bctx, value.Elem()) 62 | case reflect.Struct: 63 | iterateStruct(prefix, bctx, value) 64 | return true 65 | case reflect.Map: 66 | valueType := value.Type().Key() 67 | switch valueType.Kind() { 68 | case reflect.String: 69 | iterateStringMap(prefix, bctx, value) 70 | return true 71 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 72 | iterateIntMap(prefix, bctx, value) 73 | return true 74 | default: 75 | stringerType := reflect.TypeOf((*fmt.Stringer)(nil)).Elem() 76 | if valueType.Implements(stringerType) { 77 | iterateStringerMap(prefix, bctx, value) 78 | return true 79 | } 80 | } 81 | } 82 | return false 83 | } 84 | 85 | func iterateStringerMap(prefix []string, builder *ContextBuilder, value reflect.Value) { 86 | keys := value.MapKeys() 87 | for _, keyData := range keys { 88 | keyValue := value.MapIndex(keyData) 89 | key := append(prefix, keyData.Interface().(fmt.Stringer).String()) 90 | builder.Add(join(key), keyValue.Interface()) 91 | resolveValue(key, builder, keyValue) 92 | } 93 | } 94 | 95 | func iterateIntMap(prefix []string, builder *ContextBuilder, value reflect.Value) { 96 | keys := value.MapKeys() 97 | for _, keyData := range keys { 98 | keyValue := value.MapIndex(keyData) 99 | key := append(prefix, fmt.Sprintf("%d", keyData.Interface())) 100 | builder.Add(join(key), keyValue.Interface()) 101 | resolveValue(key, builder, keyValue) 102 | } 103 | } 104 | 105 | func iterateStringMap(prefix []string, builder *ContextBuilder, value reflect.Value) { 106 | keys := value.MapKeys() 107 | for _, keyData := range keys { 108 | keyValue := value.MapIndex(keyData) 109 | key := append(prefix, keyData.String()) 110 | builder.Add(join(key), keyValue.Interface()) 111 | resolveValue(key, builder, keyValue) 112 | } 113 | } 114 | 115 | func iterateStruct(prefix []string, builder *ContextBuilder, value reflect.Value) { 116 | for i := 0; i < value.NumField(); i++ { 117 | fieldMeta := value.Type().Field(i) 118 | fieldValue := value.Field(i) 119 | if fieldMeta.Anonymous { 120 | resolveValue(prefix, builder, fieldValue) 121 | } else { 122 | key := append(prefix, fieldMeta.Name) 123 | builder.Add(join(key), fieldValue.Interface()) 124 | resolveValue(key, builder, fieldValue) 125 | } 126 | } 127 | } 128 | 129 | func join(src []string) string { 130 | return strings.Join(src, ".") 131 | } 132 | 133 | // Formatg is a formatting with type guessing 134 | func Formatg(format string, data interface{}) string { 135 | bctx := NewContextBuilder() 136 | value := reflect.ValueOf(data) 137 | if !resolveValue(nil, bctx, value) { 138 | panic(fmt.Errorf("struct or map[string | integer | Stringer]X expected, got %T", data)) 139 | } 140 | ctx, err := bctx.Build() 141 | if err != nil { 142 | panic(err) 143 | } 144 | res, err := Format(format, ctx) 145 | if err != nil { 146 | panic(err) 147 | } 148 | return res 149 | } 150 | 151 | // Formatf is a formatting where values are taken form given func(string) string function 152 | func Formatf(format string, data func(string) string) string { 153 | ctx := contextFunc(data) 154 | res, err := Format(format, ctx) 155 | if err != nil { 156 | panic(err) 157 | } 158 | return res 159 | } 160 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestFormat(t *testing.T) { 12 | builder := NewContextBuilder() 13 | builder.AddFormatter("date", timeFormatter(time.Date(2016, 9, 10, 11, 12, 13, 0, time.UTC))) 14 | builder.AddFormatter("path", stringFormatter("/path/to/logs")) 15 | builder.AddFormatter("num", i8Formatter{ 16 | int8(12), 17 | }) 18 | context, err := builder.Build() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | res, err := Format("${path}/bos-k011/bos_srv-k011a.fss.log.${date | %Y%m%d }.gz", context) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | if !assert.Equal(t, "/path/to/logs/bos-k011/bos_srv-k011a.fss.log.20160910.gz", res) { 28 | return 29 | } 30 | 31 | res, err = Format("${path}/bos-k011/bos_srv-k011a.fss.log.${date + 1 day | %Y%m%d }.gz", context) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | if !assert.Equal(t, "/path/to/logs/bos-k011/bos_srv-k011a.fss.log.20160911.gz", res) { 36 | return 37 | } 38 | 39 | res, err = Format("${path}/bos-k011/bos_srv-k011a.fss.log.${date 1 day | %Y%m%d }.gz", context) 40 | if !assert.NotNil(t, err) { 41 | return 42 | } 43 | 44 | res, err = Format("${path}/bos-k011/bos_srv-k011a.fss.log.${date - 1 day | %Y%m%d }.gz", context) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | if !assert.Equal(t, "/path/to/logs/bos-k011/bos_srv-k011a.fss.log.20160909.gz", res) { 49 | return 50 | } 51 | 52 | res, err = Format("${path}/bos-k011/bos_srv-k011a.fss.log.${date - 2 months 1 day | %Y%m%d }.gz", context) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | if !assert.Equal(t, "/path/to/logs/bos-k011/bos_srv-k011a.fss.log.20160709.gz", res) { 57 | return 58 | } 59 | 60 | res, err = Format("${path}/bos-k011/bos_srv-k011a.fss.log.${date - 12 hours | %Y%m%d }.gz", context) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | if !assert.Equal(t, "/path/to/logs/bos-k011/bos_srv-k011a.fss.log.20160909.gz", res) { 65 | return 66 | } 67 | 68 | res, err = Format(`${path}/bos-k011/bos_srv-k011a.fss.log.${date - 12 hours | "%Y%m%d %H%M%S" }.gz`, context) 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | if !assert.Equal(t, "/path/to/logs/bos-k011/bos_srv-k011a.fss.log.20160909 231213.gz", res) { 73 | return 74 | } 75 | 76 | res, err = Format("${ num | 04 }", context) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | if !assert.Equal(t, "0012", res) { 81 | return 82 | } 83 | 84 | res, err = Format("$path$num", context) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | require.Equal(t, "/path/to/logs12", res) 89 | 90 | res, err = Format("$path abc", context) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | require.Equal(t, "/path/to/logs abc", res) 95 | 96 | res = Formatp("${+1 day|%Y-%m-%d}", time.Date(2018, 1, 15, 0, 0, 0, 0, time.UTC)) 97 | require.Equal(t, "2018-01-16", res) 98 | 99 | res = Formatp("$$a") 100 | require.Equal(t, "$a", res) 101 | } 102 | 103 | func TestClarify(t *testing.T) { 104 | moscow, err := time.LoadLocation("Europe/Moscow") 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | date := timeFormatter(time.Date(1982, 10, 19, 18, 22, 33, 0, moscow)) 109 | date2, err := date.MapDelta("+ 1 year") 110 | if err != nil { 111 | t.Fatal(err) 112 | } 113 | if !assert.Equal(t, time.Time(date).AddDate(1, 0, 0), date2) { 114 | return 115 | } 116 | } 117 | 118 | func TestFormatp(t *testing.T) { 119 | require.Equal( 120 | t, 121 | "a 2 4.5 2 2018", 122 | Formatp("${} ${} ${|1.1} ${1} ${|%Y}", "a", 2, 4.5, time.Date(2018, 10, 19, 18, 0, 5, 0, time.UTC)), 123 | ) 124 | require.Equal(t, "a a", Formatp("$ $0", "a")) 125 | require.Equal(t, "a.b", Formatp("$0.$1", "a", "b")) 126 | } 127 | 128 | func TestFormatm(t *testing.T) { 129 | require.Equal(t, "a 2 4.5", Formatm("${key} ${num} ${float|1.1}", map[string]interface{}{ 130 | "key": "a", 131 | "num": 2, 132 | "float": 4.5, 133 | })) 134 | } 135 | 136 | func TestFormatg(t *testing.T) { 137 | require.Equal(t, "a 2 4.5", Formatg("${key} ${num} ${float|1.1}", map[string]interface{}{ 138 | "key": "a", 139 | "num": 2, 140 | "float": 4.5, 141 | })) 142 | 143 | type t1 struct { 144 | A string 145 | B int 146 | } 147 | s1 := t1{ 148 | A: "a1", 149 | B: 2, 150 | } 151 | require.Equal(t, "a1→2", Formatg("${A}→$B", s1)) 152 | 153 | var s2 struct { 154 | t1 155 | C float64 156 | D t1 157 | } 158 | s2.t1 = s1 159 | s2.C = 0.5 160 | s2.D = s1 161 | require.Equal(t, "a1 2 0.5 2 a1", Formatg("$A $B ${C|1.1} ${D.B} ${D.A}", s2)) 162 | } 163 | 164 | func TestReadmeFormatp(t *testing.T) { 165 | res := Formatp("$ ${} $1 ${0}", 1, 2) 166 | require.Equal(t, "1 2 2 1", res) 167 | } 168 | 169 | func TestRegression(t *testing.T) { 170 | require.Equal(t, "1", Formatp("$", "1")) 171 | require.Equal(t, "", Formatp("", "1")) 172 | } 173 | 174 | func TestReamdeFormatm(t *testing.T) { 175 | res := Formatm("${name} $count ${weight|1.2}", Values{ 176 | "name": "name", 177 | "count": 12, 178 | "weight": 0.79, 179 | }) 180 | require.Equal(t, "name 12 0.79", res) 181 | } 182 | 183 | type T struct { 184 | A string 185 | Field int 186 | } 187 | 188 | func TestReadmeFormatg(t *testing.T) { 189 | var s = T{ 190 | A: "str", 191 | Field: 12, 192 | } 193 | var d struct { 194 | F T 195 | Entry float64 196 | } 197 | d.F = s 198 | d.Entry = 0.5 199 | res := Formatg("${F.A} ${F.Field} $Entry", d) 200 | require.Equal(t, "str 12 0.500000", res) 201 | 202 | v := map[int]string{ 203 | 1: "bc", 204 | 12: "bd", 205 | } 206 | res = Formatg("$1-$12", v) 207 | require.Equal(t, "bc-bd", res) 208 | } 209 | 210 | func TestReadmeDateArithmetics(t *testing.T) { 211 | tm := time.Date(2018, 1, 18, 22, 57, 37, 12, time.UTC) 212 | res := Formatm("${ date + 1 day | %Y-%m-%d %H:%M:%S }", Values{ 213 | "date": tm, 214 | }) 215 | require.Equal(t, "2018-01-19 22:57:37", res) 216 | } 217 | 218 | func TestFormatf(t *testing.T) { 219 | tests := []struct { 220 | name string 221 | format string 222 | data func(string) string 223 | want string 224 | }{ 225 | { 226 | name: "typical", 227 | format: "${name} ${value} ${location} - ${fake}", 228 | data: func(s string) string { 229 | switch s { 230 | case "name": 231 | return "Denis" 232 | case "value": 233 | return "1k" 234 | case "location": 235 | return "cbx" 236 | default: 237 | return "" 238 | } 239 | }, 240 | want: "Denis 1k cbx - ", 241 | }, 242 | } 243 | for _, tt := range tests { 244 | t.Run(tt.name, func(t *testing.T) { 245 | if got := Formatf(tt.format, tt.data); got != tt.want { 246 | t.Errorf("Formatf() = %v, want %v", got, tt.want) 247 | } 248 | }) 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /formatter.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | // Formatter generic formatting piece 4 | type Formatter interface { 5 | Clarify(string) (Formatter, error) 6 | Format(string) (string, error) 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sirkon/go-format/v2 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect 7 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect 8 | github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570 // indirect 9 | github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 10 | github.com/pkg/errors v0.8.1 // indirect 11 | github.com/stretchr/testify v1.3.0 12 | github.com/tebeka/strftime v0.0.0-20140926081919-3f9c7761e312 // indirect 13 | golang.org/x/net v0.0.0-20190119204137-ed066c81e75e // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= 4 | github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= 5 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4= 6 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag= 7 | github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570 h1:0iQektZGS248WXmGIYOwRXSQhD4qn3icjMpuxwO7qlo= 8 | github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570/go.mod h1:BLt8L9ld7wVsvEWQbuLrUZnCMnUmLZ+CGDzKtclrTlE= 9 | github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 h1:Bvq8AziQ5jFF4BHGAEDSqwPW1NJS3XshxbRCxtjFAZc= 10 | github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042/go.mod h1:TPpsiPUEh0zFL1Snz4crhMlBe60PYxRHr5oFF3rRYg0= 11 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 12 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 17 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 18 | github.com/tebeka/strftime v0.0.0-20140926081919-3f9c7761e312 h1:frNEkk4P8mq+47LAMvj9LvhDq01kFDUhpJZzzei8IuM= 19 | github.com/tebeka/strftime v0.0.0-20140926081919-3f9c7761e312/go.mod h1:o6CrSUtupq/A5hylbvAsdydn0d5yokJExs8VVdx4wwI= 20 | golang.org/x/net v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q= 21 | golang.org/x/net v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 22 | -------------------------------------------------------------------------------- /int_formatter.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // intFormatter formats int 9 | type intFormatter struct { 10 | value int 11 | } 12 | 13 | // Clarify ... 14 | func (f intFormatter) Clarify(a string) (Formatter, error) { 15 | if len(strings.TrimSpace(a)) != 0 { 16 | return f, fmt.Errorf("No clarification available for int8 formatter") 17 | } 18 | return f, nil 19 | } 20 | 21 | // Format ... 22 | func (f intFormatter) Format(a string) (string, error) { 23 | return fmt.Sprintf("%"+a+"d", f.value), nil 24 | } 25 | 26 | // uintFormatter formats uint 27 | type uintFormatter struct { 28 | value uint 29 | } 30 | 31 | // Clarify ... 32 | func (f uintFormatter) Clarify(a string) (Formatter, error) { 33 | if len(strings.TrimSpace(a)) != 0 { 34 | return f, fmt.Errorf("No clarification available for uint8 formatter") 35 | } 36 | return f, nil 37 | } 38 | 39 | // Format ... 40 | func (f uintFormatter) Format(a string) (string, error) { 41 | return fmt.Sprintf("%"+a+"d", f.value), nil 42 | } 43 | 44 | // i8Formatter ... 45 | type i8Formatter struct { 46 | value int8 47 | } 48 | 49 | // Clarify formatter implementation 50 | func (f i8Formatter) Clarify(a string) (Formatter, error) { 51 | if len(strings.TrimSpace(a)) != 0 { 52 | return f, fmt.Errorf("No clarification available for int8 formatter") 53 | } 54 | return f, nil 55 | } 56 | 57 | // Format formatter implementation 58 | func (f i8Formatter) Format(a string) (string, error) { 59 | return fmt.Sprintf("%"+a+"d", f.value), nil 60 | } 61 | 62 | // i16Formatter ... 63 | type i16Formatter struct { 64 | value int16 65 | } 66 | 67 | // Clarify formatter implementation 68 | func (f i16Formatter) Clarify(a string) (Formatter, error) { 69 | if len(strings.TrimSpace(a)) != 0 { 70 | return f, fmt.Errorf("No clarification available for int16 formatter") 71 | } 72 | return f, nil 73 | } 74 | 75 | // Format formatter implementation 76 | func (f i16Formatter) Format(a string) (string, error) { 77 | return fmt.Sprintf("%"+a+"d", f.value), nil 78 | } 79 | 80 | // i32Formatter ... 81 | type i32Formatter struct { 82 | value int32 83 | } 84 | 85 | // Clarify formatter implementation 86 | func (f i32Formatter) Clarify(a string) (Formatter, error) { 87 | if len(strings.TrimSpace(a)) != 0 { 88 | return f, fmt.Errorf("No clarification available for int32 formatter") 89 | } 90 | return f, nil 91 | } 92 | 93 | // Format formatter implementation 94 | func (f i32Formatter) Format(a string) (string, error) { 95 | return fmt.Sprintf("%"+a+"d", f.value), nil 96 | } 97 | 98 | // i64Formatter ... 99 | type i64Formatter struct { 100 | value int64 101 | } 102 | 103 | // Clarify formatter implementation 104 | func (f i64Formatter) Clarify(a string) (Formatter, error) { 105 | if len(strings.TrimSpace(a)) != 0 { 106 | return f, fmt.Errorf("No clarification available for int64 formatter") 107 | } 108 | return f, nil 109 | } 110 | 111 | // Format formatter implementation 112 | func (f i64Formatter) Format(a string) (string, error) { 113 | return fmt.Sprintf("%"+a+"d", f.value), nil 114 | } 115 | 116 | // u8Formatter ... 117 | type u8Formatter struct { 118 | value uint8 119 | } 120 | 121 | // Clarify formatter implementation 122 | func (f u8Formatter) Clarify(a string) (Formatter, error) { 123 | if len(strings.TrimSpace(a)) != 0 { 124 | return f, fmt.Errorf("No clarification available for uint8 formatter") 125 | } 126 | return f, nil 127 | } 128 | 129 | // Format formatter implementation 130 | func (f u8Formatter) Format(a string) (string, error) { 131 | return fmt.Sprintf("%"+a+"d", f.value), nil 132 | } 133 | 134 | // u16Formatter ... 135 | type u16Formatter struct { 136 | value uint16 137 | } 138 | 139 | // Clarify formatter implementation 140 | func (f u16Formatter) Clarify(a string) (Formatter, error) { 141 | if len(strings.TrimSpace(a)) != 0 { 142 | return f, fmt.Errorf("No clarification available for uint16 formatter") 143 | } 144 | return f, nil 145 | } 146 | 147 | // Format formatter implementation 148 | func (f u16Formatter) Format(a string) (string, error) { 149 | return fmt.Sprintf("%"+a+"d", f.value), nil 150 | } 151 | 152 | // u32Formatter ... 153 | type u32Formatter struct { 154 | value uint32 155 | } 156 | 157 | // Clarify formatter implementation 158 | func (f u32Formatter) Clarify(a string) (Formatter, error) { 159 | if len(strings.TrimSpace(a)) != 0 { 160 | return f, fmt.Errorf("No clarification available for uint32 formatter") 161 | } 162 | return f, nil 163 | } 164 | 165 | // Format formatter implementation 166 | func (f u32Formatter) Format(a string) (string, error) { 167 | return fmt.Sprintf("%"+a+"d", f.value), nil 168 | } 169 | 170 | // u64Formatter ... 171 | type u64Formatter struct { 172 | value uint64 173 | } 174 | 175 | // Clarify formatter implementation 176 | func (f u64Formatter) Clarify(a string) (Formatter, error) { 177 | if len(strings.TrimSpace(a)) != 0 { 178 | return f, fmt.Errorf("No clarification available for uint64 formatter") 179 | } 180 | return f, nil 181 | } 182 | 183 | // Format formatter implementation 184 | func (f u64Formatter) Format(a string) (string, error) { 185 | return fmt.Sprintf("%"+a+"d", f.value), nil 186 | } 187 | -------------------------------------------------------------------------------- /nope_formatter.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | type nopeFormatter struct{} 4 | 5 | // Clarify ... 6 | func (n nopeFormatter) Clarify(string) (Formatter, error) { 7 | return n, nil 8 | } 9 | 10 | // Format ... 11 | func (nopeFormatter) Format(string) (string, error) { 12 | return "", nil 13 | } 14 | -------------------------------------------------------------------------------- /split.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | // Splitter splits the string 11 | type Splitter struct { 12 | orig string 13 | rest string 14 | context Context 15 | 16 | count int 17 | 18 | err error 19 | cur string 20 | } 21 | 22 | // NewSplitter ... 23 | func NewSplitter(text string, context Context) *Splitter { 24 | return &Splitter{ 25 | orig: text, 26 | rest: text, 27 | context: context, 28 | } 29 | } 30 | 31 | func nipString(content string) (res string, rest string, err error) { 32 | content = content[1:] // Passing first character 33 | var pos int 34 | for { 35 | pos = strings.IndexByte(content, '"') 36 | if pos < 0 { 37 | err = fmt.Errorf("didn't find string end at %s", content) 38 | return 39 | } 40 | res += content[:pos] 41 | if strings.HasSuffix(res, "\\") { 42 | res += content[pos : pos+1] 43 | content = content[pos+1:] 44 | } else { 45 | content = content[pos+1:] 46 | break 47 | } 48 | } 49 | 50 | res = strings.Replace(res, "\\t", "\t", -1) 51 | res = strings.Replace(res, "\\n", "\n", -1) 52 | res = strings.Replace(res, "\\\"", "\"", -1) 53 | res = strings.Replace(res, "\\r", "\r", -1) 54 | res = strings.Replace(res, "\\\\", "\\", -1) 55 | rest = content 56 | return 57 | } 58 | 59 | func nipIdentifier(content string) (string, string, error) { 60 | if len(content) == 0 { 61 | return "", "", fmt.Errorf("expected heading identifier or }, got empty string instead") 62 | } 63 | if content[0] == '|' || content[0] == '}' { 64 | return "", content, nil 65 | } 66 | i := 0 67 | for i < len(content) && isWord(rune(content[i])) { 68 | i++ 69 | } 70 | if content[0] == '+' || content[0] == '-' { 71 | // must be date arithmetic 72 | return "", content, nil 73 | } 74 | if i == 0 { 75 | return "", "", fmt.Errorf("expected heading identifier in %s", content) 76 | } 77 | 78 | return content[:i], content[i:], nil 79 | } 80 | 81 | func isWord(r rune) bool { 82 | return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '.' 83 | } 84 | 85 | func isSimpleWord(r rune) bool { 86 | return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' 87 | } 88 | 89 | func nipOpenIdentifier(content string) (string, string, error) { 90 | i := 0 91 | for i < len(content) && unicode.IsDigit(rune(content[i])) { 92 | i++ 93 | } 94 | if i > 0 { 95 | return content[:i], content[i:], nil 96 | } 97 | for i < len(content) && isSimpleWord(rune(content[i])) { 98 | i++ 99 | } 100 | 101 | return content[:i], content[i:], nil 102 | } 103 | 104 | func locateABuck(rest string) int { 105 | var off int 106 | for i := strings.IndexByte(rest, '$'); i >= 0; i = strings.IndexByte(rest, '$') { 107 | if i == len(rest)-1 { 108 | return i + off 109 | } 110 | 111 | if rest[i+1] != '$' { 112 | return i + off 113 | } 114 | 115 | off += i + 2 116 | rest = rest[i+2:] 117 | } 118 | 119 | return -1 120 | } 121 | 122 | // Split cut the next chunk and apply context formatting once it is a piece of format 123 | func (s *Splitter) Split() bool { 124 | if len(s.rest) == 0 { 125 | return false 126 | } 127 | 128 | pos := locateABuck(s.rest) 129 | if pos < 0 { 130 | s.cur = s.rest 131 | s.rest = "" 132 | return true 133 | } else if pos > 0 { 134 | s.cur = s.rest[:pos] 135 | s.rest = s.rest[pos:] 136 | return true 137 | } 138 | 139 | var ident string 140 | var err error 141 | var formatter Formatter 142 | 143 | if len(s.rest) == 0 || s.rest == "$" { 144 | ident = strconv.Itoa(s.count) 145 | s.count++ 146 | formatter, err = s.context.GetFormatter(ident) 147 | if err != nil { 148 | s.err = err 149 | return false 150 | } 151 | s.cur, s.err = formatter.Format("") 152 | if len(s.rest) > 0 { 153 | s.rest = s.rest[len(s.rest):] 154 | } 155 | return s.err == nil 156 | } else if isWord(rune(s.rest[1])) { 157 | // This is a simple subsitution 158 | s.rest = s.rest[1:] 159 | ident, s.rest, err = nipOpenIdentifier(s.rest) 160 | if err != nil { 161 | s.err = err 162 | return false 163 | } 164 | formatter, err = s.context.GetFormatter(ident) 165 | if err != nil { 166 | s.err = err 167 | return false 168 | } 169 | s.cur, s.err = formatter.Format("") 170 | return s.err == nil 171 | } else if s.rest[1] != '{' { 172 | ident = strconv.Itoa(s.count) 173 | s.count++ 174 | formatter, err := s.context.GetFormatter(ident) 175 | if err != nil { 176 | s.err = err 177 | return false 178 | } 179 | s.cur, s.err = formatter.Format("") 180 | s.rest = s.rest[1:] 181 | return s.err == nil 182 | } 183 | 184 | // This is a format! 185 | s.rest = s.rest[2:] 186 | s.rest = strings.TrimLeft(s.rest, " \t\r\n") 187 | 188 | // Nip the leading identifier 189 | ident, s.rest, err = nipIdentifier(s.rest) 190 | if err != nil { 191 | s.err = err 192 | return false 193 | } 194 | if len(ident) == 0 { 195 | ident = strconv.Itoa(s.count) 196 | s.count++ 197 | } 198 | formatter, err = s.context.GetFormatter(ident) 199 | if err != nil { 200 | s.err = err 201 | return false 202 | } 203 | 204 | s.rest = strings.TrimLeft(s.rest, " \t\r\n") 205 | if strings.HasPrefix(s.rest, "}") { 206 | // It is just a substitution 207 | s.cur, s.err = formatter.Format("") 208 | s.rest = s.rest[1:] 209 | return s.err == nil 210 | } 211 | 212 | if !strings.HasPrefix(s.rest, "|") { 213 | // There is a clarification 214 | pos := strings.IndexByte(s.rest, '|') 215 | if pos < 0 { 216 | // Let the clarification needs a format 217 | s.err = fmt.Errorf("couldn't find clarification end in %s", s.rest) 218 | return false 219 | } 220 | clarification := s.rest[:pos] 221 | s.rest = s.rest[pos:] 222 | formatter, s.err = formatter.Clarify(clarification) 223 | if s.err != nil { 224 | return false 225 | } 226 | } 227 | 228 | s.rest = s.rest[1:] 229 | s.rest = strings.TrimLeft(s.rest, " \t\r\n") 230 | if len(s.rest) == 0 { 231 | s.err = fmt.Errorf("couldn't find format end in %s", s.rest) 232 | return false 233 | } 234 | 235 | var format string 236 | if s.rest[0] == '"' { 237 | format, s.rest, s.err = nipString(s.rest) 238 | if s.err != nil { 239 | return false 240 | } 241 | pos = strings.IndexByte(s.rest, '}') 242 | if pos < 0 { 243 | s.err = fmt.Errorf("couldn't find format end in %s", s.rest) 244 | return false 245 | } 246 | s.rest = s.rest[pos+1:] 247 | } else { 248 | pos = strings.IndexByte(s.rest, '}') 249 | if pos < 0 { 250 | s.err = fmt.Errorf("couldn't find format end in %s", s.rest) 251 | return false 252 | } 253 | format = strings.TrimSpace(s.rest[:pos]) 254 | s.rest = s.rest[pos+1:] 255 | } 256 | 257 | s.cur, s.err = formatter.Format(format) 258 | return s.err == nil 259 | } 260 | 261 | // Text ... 262 | func (s *Splitter) Text() string { 263 | return strings.Replace(s.cur, "$$", "$", -1) 264 | } 265 | 266 | // Err ... 267 | func (s *Splitter) Err() error { 268 | return s.err 269 | } 270 | -------------------------------------------------------------------------------- /split_test.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_locateABuck(t *testing.T) { 10 | tests := []struct { 11 | rest string 12 | want int 13 | }{ 14 | { 15 | rest: "$$abcd", 16 | want: -1, 17 | }, 18 | { 19 | rest: "$abcd", 20 | want: 0, 21 | }, 22 | { 23 | rest: "abcdef", 24 | want: -1, 25 | }, 26 | { 27 | rest: " $a", 28 | want: 3, 29 | }, 30 | { 31 | rest: "$$abcde $a", 32 | want: 8, 33 | }, 34 | { 35 | rest: " ${num} ${float|1.1}", 36 | want: 1, 37 | }, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.rest, func(t *testing.T) { 41 | loc := locateABuck(tt.rest) 42 | if loc >= 0 { 43 | t.Log(tt.rest[loc:]) 44 | } 45 | assert.Equalf(t, tt.want, loc, "locateABuck(%v)", tt.rest) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /string_formatter.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // stringFormatter formatting primitive 9 | type stringFormatter string 10 | 11 | // Clarify formatter implementation 12 | func (f stringFormatter) Clarify(a string) (Formatter, error) { 13 | if len(strings.TrimSpace(a)) != 0 { 14 | return f, fmt.Errorf("No clarification available for string formatter") 15 | } 16 | return f, nil 17 | } 18 | 19 | // Format formatter implementation 20 | func (f stringFormatter) Format(a string) (string, error) { 21 | if len(strings.TrimSpace(a)) != 0 { 22 | return string(f), fmt.Errorf("No clarification available for string formatter") 23 | } 24 | return string(f), nil 25 | } 26 | -------------------------------------------------------------------------------- /time_formatter.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | strftime "github.com/lestrrat/go-strftime" 10 | ) 11 | 12 | var deltaWords = []string{"year", "month", "week", "day", "hour", "minute", "second"} 13 | var pluralReduction = map[string]string{ 14 | "years": "year", "months": "month", "weeks": "week", "days": "day", "hours": "hour", 15 | "minutes": "minute", "seconds": "second", 16 | } 17 | var deltaIndexes = map[string]int{} 18 | 19 | func init() { 20 | for i, word := range deltaWords { 21 | deltaIndexes[word] = i 22 | } 23 | } 24 | 25 | func deltaIndex(singular string) int { 26 | index, ok := deltaIndexes[singular] 27 | if !ok { 28 | panic(fmt.Sprintf("Unsupported singular word %s", singular)) 29 | } 30 | return index 31 | } 32 | 33 | func restMatcher(rest []string, singular string, handler func(int) time.Time) (res time.Time, ok bool, err error) { 34 | if len(rest) == 0 { 35 | res = handler(0) 36 | return 37 | } 38 | num, err := strconv.ParseInt(rest[0], 10, 64) 39 | if err != nil { 40 | err = fmt.Errorf("Expected to see a number, got %s instead", rest[0]) 41 | return 42 | } 43 | 44 | if len(rest) == 1 { 45 | index := deltaIndex(singular) 46 | err = fmt.Errorf("Expected to see one of %s, got nothing instead", strings.Join(deltaWords[index:], ", ")) 47 | return 48 | } 49 | 50 | probableSingular, ok := pluralReduction[strings.ToLower(rest[1])] 51 | if !ok { 52 | probableSingular = strings.ToLower(rest[1]) 53 | } 54 | index, ok := deltaIndexes[probableSingular] 55 | if !ok { 56 | index = deltaIndex(singular) 57 | err = fmt.Errorf("Expected to see one of %s, got %s instead", strings.Join(deltaWords[index:], ", "), rest[1]) 58 | return 59 | } 60 | if index < deltaIndex(singular) { 61 | index = deltaIndex(singular) 62 | err = fmt.Errorf( 63 | "Wrong delta parts precedence: shorter interval (week is shorter than month or year) must follow longers ones"+ 64 | ", i.e. you cannot write `1 day 5 months` and must use `5 months 1 day` form instead. In this case we got %s"+ 65 | " while we were expecting one of %s", rest[1], strings.Join(deltaWords[index:], ", ")) 66 | return 67 | } 68 | if probableSingular == singular { 69 | return handler(int(num)), true, nil 70 | } 71 | return handler(0), false, nil 72 | } 73 | 74 | // timeFormatter formatter 75 | type timeFormatter time.Time 76 | 77 | // MapDelta returnes shifted time object compared to the original value 78 | func (d timeFormatter) MapDelta(delta string) (datetime time.Time, err error) { 79 | datetime = time.Time(d) 80 | 81 | delta = strings.TrimSpace(delta) 82 | if len(delta) == 0 { 83 | return 84 | } 85 | 86 | var sign int 87 | if strings.HasPrefix(delta, "+") { 88 | sign = 1 89 | delta = delta[1:] 90 | } else if strings.HasPrefix(delta, "-") { 91 | sign = -1 92 | delta = delta[1:] 93 | } else { 94 | err = fmt.Errorf("Expected + or - sign at the start of clarfying expression, got %s instead", delta) 95 | return 96 | } 97 | 98 | delta = strings.TrimLeft(delta, "+ \t\n\r") 99 | rest := strings.Fields(delta) 100 | var ok bool 101 | 102 | datetime, ok, err = restMatcher(rest, "year", func(k int) time.Time { return datetime.AddDate(sign*k, 0, 0) }) 103 | if err != nil { 104 | return 105 | } 106 | if ok { 107 | rest = rest[2:] 108 | } 109 | if len(rest) == 0 { 110 | return 111 | } 112 | 113 | datetime, ok, err = restMatcher(rest, "month", func(k int) time.Time { return datetime.AddDate(0, sign*k, 0) }) 114 | if err != nil { 115 | return 116 | } 117 | if ok { 118 | rest = rest[2:] 119 | } 120 | if len(rest) == 0 { 121 | return 122 | } 123 | 124 | datetime, ok, err = restMatcher(rest, "week", func(k int) time.Time { return datetime.AddDate(0, 0, 7*sign*k) }) 125 | if err != nil { 126 | return 127 | } 128 | if ok { 129 | rest = rest[2:] 130 | } 131 | if len(rest) == 0 { 132 | return 133 | } 134 | 135 | datetime, ok, err = restMatcher(rest, "day", func(k int) time.Time { return datetime.AddDate(0, 0, sign*k) }) 136 | if err != nil { 137 | return 138 | } 139 | if ok { 140 | rest = rest[2:] 141 | } 142 | if len(rest) == 0 { 143 | return 144 | } 145 | 146 | datetime, ok, err = restMatcher(rest, "hour", 147 | func(k int) time.Time { return datetime.Add(time.Duration(sign*k) * time.Second * 3600) }) 148 | if err != nil { 149 | return 150 | } 151 | if ok { 152 | rest = rest[2:] 153 | } 154 | if len(rest) == 0 { 155 | return 156 | } 157 | 158 | datetime, ok, err = restMatcher(rest, "minute", 159 | func(k int) time.Time { return datetime.Add(time.Duration(sign*k) * time.Second * 60) }) 160 | if err != nil { 161 | return 162 | } 163 | if ok { 164 | rest = rest[2:] 165 | } 166 | if len(rest) == 0 { 167 | return 168 | } 169 | 170 | datetime, ok, err = restMatcher(rest, "second", 171 | func(k int) time.Time { return datetime.Add(time.Duration(sign*k) * time.Second) }) 172 | if err != nil { 173 | return 174 | } 175 | if ok { 176 | rest = rest[2:] 177 | } 178 | if len(rest) == 0 { 179 | return 180 | } 181 | err = fmt.Errorf("Unparsed rest of the delta expression left \033[31m%s\033[0m", rest) 182 | return 183 | } 184 | 185 | // Clarify formatter implementation 186 | func (d timeFormatter) Clarify(delta string) (Formatter, error) { 187 | datetime, err := d.MapDelta(delta) 188 | if err != nil { 189 | return nil, err 190 | } 191 | return timeFormatter(datetime), err 192 | } 193 | 194 | // Format formatter implementation 195 | func (d timeFormatter) Format(format string) (string, error) { 196 | return strftime.Format(format, time.Time(d)) 197 | } 198 | -------------------------------------------------------------------------------- /value_formatter.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // valueFormatter ... 9 | type valueFormatter struct { 10 | value interface{} 11 | } 12 | 13 | // Clarify formatter implementation 14 | func (f valueFormatter) Clarify(a string) (Formatter, error) { 15 | if len(strings.TrimSpace(a)) != 0 { 16 | return f, fmt.Errorf("No clarification available for value formatters") 17 | } 18 | return f, nil 19 | } 20 | 21 | // Format formatter implementation 22 | func (f valueFormatter) Format(a string) (string, error) { 23 | return fmt.Sprintf("%"+a+"v", f.value), nil 24 | } 25 | --------------------------------------------------------------------------------