├── .github └── workflows │ ├── master-push.yml │ └── tag-push.yml ├── .gitignore ├── .golangci.yaml ├── LICENSE ├── Makefile ├── README.md ├── action_add.go ├── action_add_test.go ├── action_change_type.go ├── action_change_type_test.go ├── action_filter.go ├── action_filter_test.go ├── action_modify.go ├── action_modify_test.go ├── action_rename.go ├── action_rename_test.go ├── action_selection.go ├── action_selection_test.go ├── action_update.go ├── column.go ├── column_test.go ├── definition.go ├── error.go ├── go.mod ├── go.sum ├── io.go ├── options.go ├── row.go ├── row_test.go ├── state.go ├── table.go ├── table_access.go ├── table_action.go ├── table_clone.go ├── table_clone_test.go ├── table_display.go ├── table_input.go ├── table_output.go ├── table_output_test.go ├── table_resolve.go ├── table_resolve_test.go ├── table_slice.go ├── table_test.go ├── table_write.go ├── typecast.go └── utils.go /.github/workflows/master-push.yml: -------------------------------------------------------------------------------- 1 | name: Go Lint + Test 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: 1.24.2 19 | 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v7 22 | with: 23 | version: v2.0 24 | 25 | - name: Run Lints 26 | run: make lint 27 | 28 | - name: Run Test 29 | run: make test -------------------------------------------------------------------------------- /.github/workflows/tag-push.yml: -------------------------------------------------------------------------------- 1 | name: Update pkg.go.dev 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | update-pkg-go-dev: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Update proxy.golang.org 13 | run: GOPROXY=proxy.golang.org go list -m github.com/OpenRunic/framed@${{github.ref_name}} 14 | 15 | # see https://pkg.go.dev/about#adding-a-package 16 | - name: Update pkg.go.dev 17 | run: | 18 | ESCAPED_MODULE=$(echo $GITHUB_REPOSITORY | tr '[:upper:]' '[:lower:]') 19 | curl https://proxy.golang.org/github.com/$ESCAPED_MODULE/@v/$GITHUB_REF_NAME.info -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenRunic/framed/eea68b3314f98764f1267ab199ab2034429a0ab9/.gitignore -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | go: "1.24" 4 | linters: 5 | default: none 6 | enable: 7 | - errcheck 8 | - goconst 9 | - gocyclo 10 | - govet 11 | - ineffassign 12 | - misspell 13 | - revive 14 | - staticcheck 15 | - unconvert 16 | - unparam 17 | - unused 18 | settings: 19 | dupl: 20 | threshold: 100 21 | goconst: 22 | min-len: 2 23 | min-occurrences: 5 24 | gocyclo: 25 | min-complexity: 70 26 | misspell: 27 | locale: US 28 | exclusions: 29 | generated: lax 30 | presets: 31 | - comments 32 | - common-false-positives 33 | - legacy 34 | - std-error-handling 35 | rules: 36 | - linters: 37 | - errcheck 38 | path: table_input.go 39 | - linters: 40 | - staticcheck 41 | path: table_display.go 42 | paths: 43 | - third_party$ 44 | - builtin$ 45 | - examples$ 46 | formatters: 47 | exclusions: 48 | generated: lax 49 | paths: 50 | - third_party$ 51 | - builtin$ 52 | - examples$ 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Open Runic (https://github.com/OpenRunic) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint 2 | lint: 3 | @golangci-lint run -c .golangci.yaml ./... 4 | 5 | .PHONY: test 6 | test: 7 | @go test -v ./... \ 8 | -test.parallel 2 \ 9 | -test.timeout 5s 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Table 2 | --- 3 | 4 | ![Push Status](https://github.com/OpenRunic/framed/actions/workflows/master-push.yml/badge.svg) 5 | 6 | Data manipulation library inspired from Pandas (Python) for golang 7 | 8 | #### Download 9 | ``` 10 | go get -u github.com/OpenRunic/framed 11 | ``` 12 | 13 | #### Options 14 | --- 15 | 16 | ```go 17 | df := framed.New( 18 | 19 | // set max rows to import/load (default = -1) 20 | framed.WithMaxRows(100), 21 | 22 | // set column separater (default = ',') 23 | framed.WithSeparator(';'), 24 | 25 | // set column names for table (otherwise automatically generated from first row) 26 | framed.WithColumns([]string{"id", "col2", ... "colN"}), 27 | 28 | // set column definition 29 | framed.WithDefinitionType("id", framed.ToType(1)), 30 | 31 | // set column data type reader 32 | framed.WithTypeReader(func(idx int, value string) reflect.Type { 33 | return framed.ToType(bool) 34 | }), 35 | ) 36 | ``` 37 | 38 | #### Load data 39 | --- 40 | Option callbacks can be used as above on table loaders below 41 | 42 | ```go 43 | // load table from file 44 | table, err := framed.File("path_to_file.csv") 45 | 46 | // load table from url 47 | table, err := framed.URL("http_https_url...csv") 48 | 49 | // load table from io.Reader 50 | table, err := framed.Reader(io.Reader) 51 | 52 | // load table from slice of string 53 | table, err := framed.Lines([]string{"1....", "2...."}) 54 | 55 | // load table from slice of raw data 56 | table, err := framed.Raw([][]string{{"1", "..."}, {"2", "..."}}) 57 | ``` 58 | 59 | #### Actions Pipeline 60 | --- 61 | Run set of pipeline actions to modify data in the table 62 | 63 | ```go 64 | // add column action 65 | addAction := framed.AddColumn("name", "", func(s *framed.State, r *framed.Row) string { 66 | return fmt.Sprintf("%s %s", r.At(s.Index("last_name")), r.At(s.Index("first_name"))) 67 | }) 68 | 69 | // drop column action 70 | dropAction := framed.DropColumn("last_name", "age") 71 | 72 | // rename column action 73 | renameAction := framed.RenameColumn("fname", "first_name") 74 | 75 | // pick specific columns action 76 | pickAction := framed.PickColumn("first_name", "last_name") 77 | 78 | // omit specific columns action 79 | omitAction := framed.PickColumn("id", "age") 80 | 81 | // make specific changes to table or its options while in pipeline action 82 | updateAction := framed.ChangeOptions(func(tbl *Table) (*Table, error) { 83 | return tbl, nil 84 | }) 85 | 86 | // change column type action 87 | changeColTypeAction := framed.ChangeColumnType("age", "", func(s *framed.State, r *framed.Row, a any) string { 88 | v := a.(int64) 89 | if v > 18 { 90 | return "adult" 91 | } else if v > 12 { 92 | return "teen" 93 | } 94 | 95 | return "kid" 96 | }) 97 | 98 | // modify every table rows action 99 | modifyAction := framed.ModifyRow(func(s *framed.State, r *framed.Row) *framed.Row { 100 | r.Set(0, framed.ColumnValue(r, 0, 0)+10) // add 10 to every row's column at index 0 101 | return r 102 | }) 103 | 104 | // filter rows from table action 105 | filterAction := framed.FilterRow(func(s *framed.State, r *framed.Row) bool { 106 | return r.Index > 9 // ignore first 10 rows 107 | }) 108 | 109 | // execute actions to build new table from result 110 | newTable, err := table.Execute( 111 | addAction, 112 | dropAction, 113 | renameAction, 114 | pickAction, 115 | omitAction, 116 | updateAction, 117 | changeColTypeAction, 118 | modifyAction, 119 | filterAction, 120 | ) 121 | ``` 122 | 123 | #### Table/Row utilities 124 | --- 125 | 126 | ```go 127 | // manually set columns for table 128 | table.UseColumns([]string{"col1", "col2", ...., "colN"}) 129 | 130 | // add row to table 131 | table.AddRow(&framed.Row{...}) 132 | 133 | // get first rows as new table 134 | df := table.Chunk(0, 100) 135 | 136 | // add line to table 137 | err := table.InsertLine("a..,b..,..z") 138 | 139 | // add slice of strings as line 140 | err := table.InsertSlice([]string{"a..", "b...", "...", "...z"}) 141 | 142 | // add lines from reader to table 143 | err := table.Read(io.Reader) 144 | 145 | // get column definition 146 | def := table.State.Definition("col1") 147 | 148 | // get column definition by index 149 | def := table.State.DefinitionAt(0) 150 | 151 | // loop through chunks of tables with 100 rows each 152 | for _, cTable := range table.Chunks(100) { 153 | ... 154 | } 155 | 156 | // get first 100 rows as []*framed.Row 157 | rows := table.Slice(0, 100) 158 | 159 | // get first row 160 | table.First() 161 | 162 | // get last row 163 | table.Last() 164 | 165 | // get length of rows 166 | table.Length() 167 | 168 | // get row at x index 169 | row := table.At(x) 170 | 171 | // get column at x index of row 172 | colValue := row.At(0) 173 | 174 | // set column value at x index of row 175 | row.Set(0, "updated_value") 176 | 177 | // patch column value at x index of row with type-safety 178 | err := row.Patch(0, "updated_value") 179 | 180 | // pick only selected columns from row 181 | columns, err := row.Pick("col2", "col3") 182 | 183 | // clone row with selected columns 184 | newRow, err := row.CloneP("col2", "col3") 185 | ``` 186 | 187 | #### Other Utilities 188 | --- 189 | 190 | ```go 191 | // extract reflect.Type from any given value 192 | framed.ToType(10) 193 | 194 | // read typesafe column value from column as index 0 195 | value := framed.ColumnValue[string](row, 0, "default_value") 196 | ``` 197 | 198 | ### Support 199 | 200 | You can file an [Issue](https://github.com/OpenRunic/framed/issues/new). 201 | 202 | ### Contribute 203 | 204 | To contrib to this project, you can open a PR or an issue. 205 | -------------------------------------------------------------------------------- /action_add.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type AddColumnDataReader[T any] = func(*State, *Row) T 8 | 9 | type ActionAddColumn[T any] struct { 10 | name string 11 | dataType reflect.Type 12 | callback AddColumnDataReader[T] 13 | } 14 | 15 | func (a ActionAddColumn[T]) ActionName() string { 16 | return "add_column" 17 | } 18 | 19 | func (a ActionAddColumn[T]) Execute(src *Table) (*Table, error) { 20 | df := src.CloneE() 21 | pos := src.ColLength() 22 | 23 | df.ResolveDefinition(a.name, a.dataType) 24 | df.AppendColumn(pos, a.name) 25 | 26 | for _, r := range src.Rows { 27 | row := r.Clone() 28 | df.AddRow(row.AddColumn(a.callback(df.State, row))) 29 | } 30 | 31 | return df, nil 32 | } 33 | 34 | // AddColumn adds new column to every rows and resolves 35 | // column value using callback and generates new table. 36 | // Generic type of [T any] is applied. 37 | // 38 | // $0 : Column Name 39 | // $1 : Sample Value of T 40 | // $2 : func(*framed.State, *framed.Row) T 41 | // 42 | // newTable, err := table.Execute( 43 | // ... 44 | // framed.AddColumn($0, $1, $2), 45 | // ... 46 | // ) 47 | func AddColumn[T any](name string, sample T, cb AddColumnDataReader[T]) *ActionAddColumn[T] { 48 | return &ActionAddColumn[T]{ 49 | name: name, 50 | dataType: ToType(sample), 51 | callback: cb, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /action_add_test.go: -------------------------------------------------------------------------------- 1 | package framed_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/OpenRunic/framed" 8 | ) 9 | 10 | func TestActionAddColumn(t *testing.T) { 11 | df := SampleTestTable() 12 | newDF, err := df.Execute( 13 | framed.AddColumn("name", "", func(s *framed.State, r *framed.Row) string { 14 | return fmt.Sprintf("%s %s", r.At(s.Index("last_name")), r.At(s.Index("first_name"))) 15 | }), 16 | ) 17 | 18 | if err != nil { 19 | t.Error(err) 20 | } else { 21 | if !newDF.State.HasColumn("name") { 22 | t.Error("expected new column in table but found none") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /action_change_type.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type ChangeColumnDataReader[T any] = func(*State, *Row, any) T 8 | 9 | type ActionChangeColumnType[T any] struct { 10 | name string 11 | dataType reflect.Type 12 | callback ChangeColumnDataReader[T] 13 | } 14 | 15 | func (a ActionChangeColumnType[T]) ActionName() string { 16 | return "change_column_type" 17 | } 18 | 19 | func (a ActionChangeColumnType[T]) Execute(src *Table) (*Table, error) { 20 | df := src.CloneE() 21 | colIndex := df.State.Index(a.name) 22 | 23 | typeChanged := a.dataType != df.State.DataType(a.name) 24 | if typeChanged { 25 | df.SetDefinition(a.name, NewDefinition(a.dataType)) 26 | } 27 | 28 | for _, r := range src.Rows { 29 | row := r.Clone() 30 | df.AddRow(row.Set(colIndex, a.callback(df.State, row, row.At(colIndex)))) 31 | } 32 | 33 | return df, nil 34 | } 35 | 36 | // ChangeColumnType updates columns on every rows and resolves 37 | // column value using callback and generates new table. 38 | // Generic type of [T any] is applied. 39 | // 40 | // $0 : Column Name 41 | // $1 : Sample Value of T 42 | // $2 : func(*framed.State, *framed.Row, any) T 43 | // 44 | // newTable, err := table.Execute( 45 | // ... 46 | // framed.ChangeColumnType($0, $1, $2), 47 | // ... 48 | // ) 49 | func ChangeColumnType[T any](name string, sample T, cb ChangeColumnDataReader[T]) *ActionChangeColumnType[T] { 50 | return &ActionChangeColumnType[T]{ 51 | name: name, 52 | dataType: ToType(sample), 53 | callback: cb, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /action_change_type_test.go: -------------------------------------------------------------------------------- 1 | package framed_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/OpenRunic/framed" 7 | ) 8 | 9 | func TestActionChangeColumnType(t *testing.T) { 10 | df := SampleTestTable() 11 | newDF, err := df.Execute( 12 | framed.ChangeColumnType("age", "", func(_ *framed.State, _ *framed.Row, a any) string { 13 | v := a.(int64) 14 | if v > 18 { 15 | return "adult" 16 | } else if v > 12 { 17 | return "teen" 18 | } 19 | 20 | return "kid" 21 | }), 22 | ) 23 | 24 | if err != nil { 25 | t.Error(err) 26 | } else { 27 | if newDF.State.DataType("age") != framed.ToType("") { 28 | t.Error("expected column in table to be of type string") 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /action_filter.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | type ActionFilterRow struct { 4 | callback func(*State, *Row) bool 5 | } 6 | 7 | func (a ActionFilterRow) ActionName() string { 8 | return "filter" 9 | } 10 | 11 | func (a ActionFilterRow) Execute(src *Table) (*Table, error) { 12 | if src.IsEmpty() { 13 | return src.CloneE(), nil 14 | } 15 | 16 | idx := 0 17 | var filtered = make([]*Row, 0) 18 | for _, row := range src.Rows { 19 | if a.callback(src.State, row) { 20 | filtered = append(filtered, row.Clone().WithIndex(idx)) 21 | idx++ 22 | } 23 | } 24 | 25 | return CherryPick(src, src.State.Columns, filtered) 26 | } 27 | 28 | // FilterRow iterates through all rows, filters the rows 29 | // and build a new table. 30 | // 31 | // newTable, err := table.Execute( 32 | // ... 33 | // framed.FilterRow(func(*framed.State, *framed.Row) bool), 34 | // ... 35 | // ) 36 | func FilterRow(cb func(*State, *Row) bool) *ActionFilterRow { 37 | return &ActionFilterRow{cb} 38 | } 39 | -------------------------------------------------------------------------------- /action_filter_test.go: -------------------------------------------------------------------------------- 1 | package framed_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/OpenRunic/framed" 7 | ) 8 | 9 | func TestTableFilterRows(t *testing.T) { 10 | limit := 10 11 | df := SampleTestTable() 12 | newDF, err := df.Execute( 13 | framed.FilterRow(func(_ *framed.State, r *framed.Row) bool { 14 | return r.Index < limit 15 | }), 16 | ) 17 | 18 | if err != nil { 19 | t.Error(err) 20 | } else { 21 | if newDF.Length() > limit { 22 | t.Error("expected rows to be filtered as provided") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /action_modify.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | type ModifyTableDataReader = func(*State, *Row) *Row 4 | 5 | type ActionModifyRow struct { 6 | callback ModifyTableDataReader 7 | } 8 | 9 | func (a ActionModifyRow) ActionName() string { 10 | return "modify_row" 11 | } 12 | 13 | func (a ActionModifyRow) Execute(src *Table) (*Table, error) { 14 | df := src.CloneE() 15 | 16 | for _, r := range src.Rows { 17 | row := r.Clone() 18 | df.AddRow(a.callback(src.State, row)) 19 | } 20 | 21 | return df, nil 22 | } 23 | 24 | // ModifyRow iterates through all rows to modify the 25 | // row and generates the new table. 26 | // 27 | // newTable, err := table.Execute( 28 | // ... 29 | // framed.ModifyRow(func(*State, *Row) *Row), 30 | // ... 31 | // ) 32 | func ModifyRow(cb ModifyTableDataReader) *ActionModifyRow { 33 | return &ActionModifyRow{ 34 | callback: cb, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /action_modify_test.go: -------------------------------------------------------------------------------- 1 | package framed_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/OpenRunic/framed" 7 | ) 8 | 9 | func TestTableModifyRows(t *testing.T) { 10 | inc := 100 11 | df := SampleTestTable() 12 | newDF, err := df.Execute( 13 | framed.ModifyRow(func(_ *framed.State, r *framed.Row) *framed.Row { 14 | return r.Set(0, framed.ColumnValue(r, 0, 0)+inc) 15 | }), 16 | ) 17 | 18 | if err != nil { 19 | t.Error(err) 20 | } else { 21 | fid := framed.ColumnValue(newDF.First(), 0, 0) 22 | if fid < inc { 23 | t.Error("expected rows to be modified as provided") 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /action_rename.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import "fmt" 4 | 5 | type ActionRenameColumn struct { 6 | pairs [][]string 7 | } 8 | 9 | func (a ActionRenameColumn) ActionName() string { 10 | return "rename_column" 11 | } 12 | 13 | func (a ActionRenameColumn) Execute(src *Table) (*Table, error) { 14 | df := src.Clone() 15 | 16 | for _, pair := range a.pairs { 17 | idx := df.State.Index(pair[0]) 18 | if idx > -1 { 19 | df.State.Columns[idx] = pair[1] 20 | df.State.Indexes[pair[1]] = idx 21 | df.State.Definitions[pair[1]] = df.State.Definition(pair[0]) 22 | 23 | delete(df.State.Indexes, pair[0]) 24 | delete(df.State.Definitions, pair[0]) 25 | } else { 26 | return nil, fmt.Errorf("unable to locate column: %s", pair[0]) 27 | } 28 | } 29 | 30 | return df, nil 31 | } 32 | 33 | // RenameColumn renames a column generates the new table. 34 | // 35 | // $0 : Existing Column Name 36 | // $1 : New Column Name 37 | // 38 | // newTable, err := table.Execute( 39 | // ... 40 | // framed.RenameColumn($0, $1), 41 | // ... 42 | // ) 43 | func RenameColumn(name string, newName string) *ActionRenameColumn { 44 | return &ActionRenameColumn{ 45 | pairs: [][]string{ 46 | {name, newName}, 47 | }, 48 | } 49 | } 50 | 51 | // RenameColumns renames multiple columns at once and 52 | // generates the new table. 53 | // 54 | // newTable, err := table.Execute( 55 | // ... 56 | // framed.RenameColumns([]string{"EXISTING_COL_NAME", "NEW_COLUMN_NAME"}, ...), 57 | // ... 58 | // ) 59 | func RenameColumns(pairs ...[]string) *ActionRenameColumn { 60 | return &ActionRenameColumn{ 61 | pairs: pairs, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /action_rename_test.go: -------------------------------------------------------------------------------- 1 | package framed_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/OpenRunic/framed" 7 | ) 8 | 9 | func TestTableRenameColumn(t *testing.T) { 10 | df := SampleTestTable() 11 | newDF, err := df.Execute( 12 | framed.RenameColumn("age", "just_a_num"), 13 | ) 14 | 15 | if err != nil { 16 | t.Error(err) 17 | } else { 18 | if newDF.State.HasColumn("age") { 19 | t.Error("failed to rename table column") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /action_selection.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | type ActionSelection struct { 4 | name string 5 | columns []string 6 | callback func(*Table, []string) []string 7 | } 8 | 9 | func (a ActionSelection) ActionName() string { 10 | if len(a.name) > 0 { 11 | return a.name 12 | } 13 | return "selection" 14 | } 15 | 16 | func (a ActionSelection) readColumns(src *Table) []string { 17 | if a.callback != nil { 18 | return a.callback(src, a.columns) 19 | } 20 | return a.columns 21 | } 22 | 23 | func (a ActionSelection) Execute(src *Table) (*Table, error) { 24 | return CherryPick(src, a.readColumns(src), src.Rows) 25 | } 26 | 27 | func ColumnSelection(name string, columns ...string) *ActionSelection { 28 | return &ActionSelection{ 29 | name: name, 30 | columns: columns, 31 | } 32 | } 33 | 34 | func ColumnSelectionCallback(name string, callback func(*Table, []string) []string, columns ...string) *ActionSelection { 35 | return &ActionSelection{name, columns, callback} 36 | } 37 | 38 | // PickColumn plucks the specified columns and 39 | // generates the new table. 40 | // 41 | // newTable, err := table.Execute( 42 | // ... 43 | // framed.PickColumn("col1", "col2", ...), 44 | // ... 45 | // ) 46 | func PickColumn(columns ...string) *ActionSelection { 47 | return ColumnSelection("pick_column", columns...) 48 | } 49 | 50 | // DropColumn ignores the specified columns and 51 | // generates the new table. 52 | // 53 | // newTable, err := table.Execute( 54 | // ... 55 | // framed.DropColumn("col1", "col2", ...), 56 | // ... 57 | // ) 58 | func DropColumn(columns ...string) *ActionSelection { 59 | return ColumnSelectionCallback("drop_column", func(src *Table, s []string) []string { 60 | return SliceOmit(src.State.Columns, s) 61 | }, columns...) 62 | } 63 | -------------------------------------------------------------------------------- /action_selection_test.go: -------------------------------------------------------------------------------- 1 | package framed_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/OpenRunic/framed" 8 | ) 9 | 10 | func TestTablePickColumns(t *testing.T) { 11 | cols := []string{"first_name", "age"} 12 | df := SampleTestTable() 13 | newDF, err := df.Execute( 14 | framed.PickColumn(cols...), 15 | ) 16 | 17 | if err != nil { 18 | t.Error(err) 19 | } else { 20 | if !reflect.DeepEqual(newDF.State.Columns, cols) { 21 | t.Error("failed to pick columns") 22 | } 23 | } 24 | } 25 | 26 | func TestTableDropColumns(t *testing.T) { 27 | df := SampleTestTable() 28 | newDF, err := df.Execute( 29 | framed.DropColumn("id", "last_name"), 30 | ) 31 | 32 | if err != nil { 33 | t.Error(err) 34 | } else { 35 | if newDF.State.HasColumn("id") || newDF.State.HasColumn("last_name") { 36 | t.Error("failed to drop columns") 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /action_update.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | type ActionUpdate struct { 4 | callback func(*Table) (*Table, error) 5 | } 6 | 7 | func (a ActionUpdate) Execute(src *Table) (*Table, error) { 8 | return a.callback(src) 9 | } 10 | 11 | // ChangeOptions provides access to current table in pipeline 12 | // and allows to change options and definitions as required. 13 | // 14 | // newTable, err := table.Execute( 15 | // ... 16 | // framed.ChangeOptions(func(*Table) (*Table, error)), 17 | // ... 18 | // ) 19 | func ChangeOptions(cb func(*Table) (*Table, error)) *ActionUpdate { 20 | return &ActionUpdate{cb} 21 | } 22 | -------------------------------------------------------------------------------- /column.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | // SplitAtChar splits string to slice separated 10 | // by provided separator but respects quotes 11 | // 12 | // $0 : string to split 13 | // $1 : separator to split the string 14 | // 15 | // splits := SplitAtChar($0, $1) 16 | func SplitAtChar(s string, sep byte) []string { 17 | var beg int 18 | var inString bool 19 | res := []string{} 20 | 21 | for i := range len(s) { 22 | if s[i] == sep && !inString { 23 | res = append(res, strings.Trim(s[beg:i], "\"")) 24 | beg = i + 1 25 | } else if s[i] == '"' { 26 | if !inString { 27 | inString = true 28 | } else if i > 0 && s[i-1] != '\\' { 29 | inString = false 30 | } 31 | } 32 | } 33 | 34 | return append(res, strings.Trim(s[beg:], "\"")) 35 | } 36 | 37 | // JoinAtChar joins slice of string to string joined 38 | // by provided separator and add quotes when needed 39 | // 40 | // $0 : slice of String to join 41 | // $1 : joining to split the string 42 | // 43 | // splits := JoinAtChar($0, $1) 44 | func JoinAtChar(ss []string, sep byte) string { 45 | res := "" 46 | 47 | sepStr := string(sep) 48 | for i, s := range ss { 49 | if i > 0 { 50 | res += sepStr 51 | } 52 | 53 | if strings.Contains(s, sepStr) { 54 | res += fmt.Sprintf("\"%s\"", s) 55 | } else { 56 | res += s 57 | } 58 | } 59 | 60 | return res 61 | } 62 | 63 | // TryColumnValue will try to read the value of column as 64 | // provided data type or else create error message. 65 | // 66 | // $0 : row instance from table 67 | // $1 : index of the column 68 | // 69 | // val, err := TryColumnValue[T any]($0, $1) 70 | func TryColumnValue[T any](row *Row, idx int) (T, error) { 71 | isPtr := false 72 | val := row.At(idx) 73 | tp := reflect.TypeOf(val) 74 | if tp.Kind() == reflect.Ptr { 75 | isPtr = true 76 | } 77 | 78 | sample := ToType[*T](nil).Elem() 79 | if isPtr { 80 | sample = ToType[*T](nil) 81 | } 82 | 83 | if tp != sample { 84 | var v any = "" 85 | return v.(T), ColError( 86 | row.Index, idx, "", 87 | fmt.Errorf("get column value failed; %s != %s", tp, sample), 88 | "read_value", 89 | ) 90 | } 91 | 92 | return val.(T), nil 93 | } 94 | 95 | // ColumnValue will try to read the value of column as 96 | // provided data type or else returns fallback value. 97 | // 98 | // $0 : row instance from table 99 | // $1 : index of the column 100 | // $2 : fallback value of the type 101 | // 102 | // val, err := ColumnValue[T any]($0, $1, $2) 103 | func ColumnValue[T any](row *Row, idx int, def T) T { 104 | val, err := TryColumnValue[T](row, idx) 105 | if err != nil { 106 | return def 107 | } 108 | 109 | return val 110 | } 111 | -------------------------------------------------------------------------------- /column_test.go: -------------------------------------------------------------------------------- 1 | package framed_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/OpenRunic/framed" 7 | ) 8 | 9 | var testColSplitValue = `aa,bb,cc,dd,"test value, data value"` 10 | 11 | func TestColumnSplits(t *testing.T) { 12 | splits := framed.SplitAtChar(testColSplitValue, ',') 13 | 14 | if len(splits) != 5 { 15 | t.Error("failed to split columns as expected") 16 | } 17 | } 18 | 19 | func TestColumnValuesJoin(t *testing.T) { 20 | line := framed.JoinAtChar( 21 | []string{"aa", "bb", "cc", "dd", "test value, data value"}, 22 | ',', 23 | ) 24 | 25 | if line != testColSplitValue { 26 | t.Error("failed to join columns as expected") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /definition.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // Definition defines details and encoder/decoder for values 8 | type Definition struct { 9 | Label string 10 | Type reflect.Type 11 | Encoder func(any) (string, error) 12 | Decoder func(string) (any, error) 13 | } 14 | 15 | // Kind returns the reflect.Kind from stored reflect.Type 16 | func (d *Definition) Kind() reflect.Kind { 17 | return d.Type.Kind() 18 | } 19 | 20 | // WithLabel changes the label for current definition 21 | func (d *Definition) WithLabel(val string) *Definition { 22 | d.Label = val 23 | return d 24 | } 25 | 26 | // WithEncoder updates the encoder to use for data 27 | func (d *Definition) WithEncoder(cb func(any) (string, error)) *Definition { 28 | d.Encoder = cb 29 | return d 30 | } 31 | 32 | // WithEncoder updates the decoder to use for data 33 | func (d *Definition) WithDecoder(cb func(string) (any, error)) *Definition { 34 | d.Decoder = cb 35 | return d 36 | } 37 | 38 | // NewDefinition creates new definition instance with reflect.Type 39 | func NewDefinition(tp reflect.Type) *Definition { 40 | return &Definition{ 41 | Type: tp, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import "fmt" 4 | 5 | // defines custom error with details about the table, row and column (if any) 6 | type TableError struct { 7 | Row int 8 | Col int 9 | ColName string 10 | Reason string 11 | Source error 12 | } 13 | 14 | func (e TableError) Error() string { 15 | msg := e.Source.Error() 16 | 17 | if e.Row > -1 { 18 | msg += fmt.Sprintf(" [row=%d]", e.Row) 19 | } 20 | if e.Col > -1 { 21 | if len(e.ColName) > 0 { 22 | msg += fmt.Sprintf(" [col=(%d) %s]", e.Col, e.ColName) 23 | } else { 24 | msg += fmt.Sprintf(" [col=%d]", e.Col) 25 | } 26 | } 27 | if len(e.Reason) > 0 { 28 | msg += fmt.Sprintf(" [reason=%s]", e.Reason) 29 | } 30 | 31 | return msg 32 | } 33 | 34 | // NewError will create an error instance along with the reason. 35 | // 36 | // $0 : main originating error 37 | // $1 : reason of the error 38 | // 39 | // err := NewError($0, $1, $2, $3, $4) 40 | func NewError(err error, reason string) TableError { 41 | return TableError{ 42 | Reason: reason, 43 | Row: -1, 44 | Source: err, 45 | } 46 | } 47 | 48 | // RowError will create an error instance with info about 49 | // row along with the reason. 50 | // 51 | // $0 : index of row 52 | // $1 : main originating error 53 | // $2 : reason of the error 54 | // 55 | // err := RowError($0, $1, $2, $3, $4) 56 | func RowError(idx int, err error, reason string) TableError { 57 | return TableError{ 58 | Row: idx, 59 | Source: err, 60 | Reason: reason, 61 | } 62 | } 63 | 64 | // ColError will create an error instance with info about row 65 | // and column index and name(if provided) along with the reason. 66 | // 67 | // $0 : index of row 68 | // $1 : index of column 69 | // $2 : name of column 70 | // $3 : main originating error 71 | // $4 : reason of the error 72 | // 73 | // err := ColError($0, $1, $2, $3, $4) 74 | func ColError(idx int, idx2 int, col string, err error, reason string) *TableError { 75 | return &TableError{ 76 | Row: idx, 77 | Col: idx2, 78 | ColName: col, 79 | Reason: reason, 80 | Source: err, 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/OpenRunic/framed 2 | 3 | go 1.24.2 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenRunic/framed/eea68b3314f98764f1267ab199ab2034429a0ab9/go.sum -------------------------------------------------------------------------------- /io.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "iter" 7 | ) 8 | 9 | // convert data from reader to iterable bytes 10 | func ReaderToLines(r io.Reader) iter.Seq[[]byte] { 11 | return func(yield func([]byte) bool) { 12 | sc := bufio.NewScanner(r) 13 | if err := sc.Err(); err != nil { 14 | return 15 | } 16 | 17 | for sc.Scan() { 18 | if !yield(sc.Bytes()) { 19 | return 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "maps" 5 | "reflect" 6 | "slices" 7 | ) 8 | 9 | // Options defines settings for table data 10 | type Options struct { 11 | 12 | // sample rows count 13 | Sampling int 14 | 15 | // printable sample size 16 | SampleSize int 17 | 18 | // stop import at max defined count 19 | MaxRows int 20 | 21 | // ignore the header line 22 | IgnoreHeader bool 23 | 24 | // column separater character 25 | Separator byte 26 | 27 | // pre-defined columns for table 28 | Columns []string 29 | 30 | // pre-defined column definitions 31 | Definitions map[string]*Definition 32 | 33 | // helper to read column type 34 | TypeReader func(int, string) reflect.Type 35 | } 36 | 37 | // OptionCallback defines function signature for option builder 38 | type OptionCallback = func(*Options) 39 | 40 | // Clone duplicates the options as new instance 41 | func (o *Options) Clone() *Options { 42 | return &Options{ 43 | MaxRows: o.MaxRows, 44 | Separator: o.Separator, 45 | Sampling: o.Sampling, 46 | SampleSize: o.SampleSize, 47 | IgnoreHeader: o.IgnoreHeader, 48 | Columns: slices.Clone(o.Columns), 49 | Definitions: maps.Clone(o.Definitions), 50 | TypeReader: o.TypeReader, 51 | } 52 | } 53 | 54 | // NewOptions creates option's instance using [OptionCallback] 55 | func NewOptions(ocbs ...OptionCallback) *Options { 56 | options := &Options{ 57 | Sampling: 2, 58 | SampleSize: 10, 59 | MaxRows: -1, 60 | Separator: ',', 61 | IgnoreHeader: false, 62 | Definitions: make(map[string]*Definition, 0), 63 | } 64 | 65 | for _, cb := range ocbs { 66 | cb(options) 67 | } 68 | 69 | return options 70 | } 71 | 72 | func WithIgnoreHeader(ih bool) OptionCallback { 73 | return func(o *Options) { 74 | o.IgnoreHeader = ih 75 | } 76 | } 77 | 78 | func WithMaxRows(s int) OptionCallback { 79 | return func(o *Options) { 80 | o.MaxRows = s 81 | } 82 | } 83 | 84 | func WithSampling(s int) OptionCallback { 85 | return func(o *Options) { 86 | o.Sampling = s 87 | } 88 | } 89 | 90 | func WithSampleSize(s int) OptionCallback { 91 | return func(o *Options) { 92 | o.SampleSize = s 93 | } 94 | } 95 | 96 | func WithSeparator(sep byte) OptionCallback { 97 | return func(o *Options) { 98 | o.Separator = sep 99 | } 100 | } 101 | 102 | func WithColumns(cols ...string) OptionCallback { 103 | return func(o *Options) { 104 | o.Columns = cols 105 | } 106 | } 107 | 108 | func WithTypeReader(cb func(int, string) reflect.Type) OptionCallback { 109 | return func(o *Options) { 110 | o.TypeReader = cb 111 | } 112 | } 113 | 114 | func WithDefinition(name string, def *Definition) OptionCallback { 115 | return func(o *Options) { 116 | o.Definitions[name] = def 117 | } 118 | } 119 | 120 | func WithDefinitionType(name string, tp reflect.Type) OptionCallback { 121 | return func(o *Options) { 122 | o.Definitions[name] = NewDefinition(tp) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /row.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | ) 7 | 8 | // Row holds columns of row in table 9 | type Row struct { 10 | 11 | // index of row in table 12 | Index int 13 | 14 | // slice of column data 15 | Columns []any 16 | } 17 | 18 | // WithIndex updates the row index in table 19 | func (r *Row) WithIndex(idx int) *Row { 20 | r.Index = idx 21 | return r 22 | } 23 | 24 | // AddColumn appends new column value to row 25 | func (r *Row) AddColumn(value any) *Row { 26 | r.Columns = append(r.Columns, value) 27 | return r 28 | } 29 | 30 | // At gives access to column at x index 31 | func (r *Row) At(idx int) any { 32 | return r.Columns[idx] 33 | } 34 | 35 | // Set updates column value at x index 36 | func (r *Row) Set(idx int, value any) *Row { 37 | r.Columns[idx] = value 38 | return r 39 | } 40 | 41 | // Patch attempts to update column value at x index 42 | // and throws error on type fail 43 | func (r *Row) Patch(def *Definition, idx int, value any) error { 44 | tp := ToType(value) 45 | if def.Type != tp { 46 | return ColError( 47 | r.Index, idx, "", 48 | fmt.Errorf("set column value failed; %s != %s", def.Type, tp), 49 | "write_value", 50 | ) 51 | } 52 | 53 | r.Set(idx, value) 54 | return nil 55 | } 56 | 57 | // AsSlice encodes columns to slice of strings or throws error 58 | func (r *Row) AsSlice(s *State) ([]string, error) { 59 | colCount := len(r.Columns) 60 | values := make([]string, colCount) 61 | 62 | for i := range colCount { 63 | val, err := ColumnValueEncoder(s.DefinitionAt(i), r.At(i)) 64 | if err != nil { 65 | return nil, ColError(r.Index, i, s.ColumnName(i), err, "value_encode") 66 | } 67 | values[i] = val 68 | } 69 | 70 | return values, nil 71 | } 72 | 73 | // Pick selects provided columns from the row or throws error 74 | func (r *Row) Pick(s *State, names ...string) ([]any, error) { 75 | cols := slices.Clone(names) 76 | if len(cols) < 1 { 77 | cols = slices.Clone(s.Columns) 78 | } 79 | 80 | columns := make([]any, 0) 81 | 82 | for _, col := range cols { 83 | idx := s.Index(col) 84 | if idx > -1 { 85 | columns = append(columns, r.At(idx)) 86 | } else { 87 | return nil, RowError(r.Index, fmt.Errorf("unknown column [%s]", col), "not_found") 88 | } 89 | } 90 | 91 | return columns, nil 92 | } 93 | 94 | // CloneP create duplicate row from selected columns 95 | func (r *Row) CloneP(s *State, names ...string) (*Row, error) { 96 | columns, err := r.Pick(s, names...) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | return &Row{ 102 | Index: r.Index, 103 | Columns: columns, 104 | }, nil 105 | } 106 | 107 | // Clone duplicates the row as-is 108 | func (r *Row) Clone() *Row { 109 | return &Row{ 110 | Index: r.Index, 111 | Columns: slices.Clone(r.Columns), 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /row_test.go: -------------------------------------------------------------------------------- 1 | package framed_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestTableRowPutValue(t *testing.T) { 9 | colIndex := 3 10 | df := SampleTestTable() 11 | row := df.First() 12 | def := df.State.DefinitionAt(colIndex) 13 | err := row.Patch(def, colIndex, "") 14 | 15 | if err == nil { 16 | t.Error("expected error on invalid value set, got none") 17 | } else { 18 | r, err := row.CloneP(df.State, "age") 19 | if err != nil { 20 | t.Error(err) 21 | } else { 22 | if reflect.TypeOf(r.At(0)).Kind() != reflect.Int64 { 23 | t.Error("failed to clone and pick column from existing row") 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /state.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "maps" 5 | "reflect" 6 | "slices" 7 | ) 8 | 9 | // columns index cache type 10 | type IndexCache = map[string]int 11 | 12 | // current state of table 13 | type State struct { 14 | 15 | // list of table columns 16 | Columns []string 17 | 18 | // indexes of detected columns 19 | Indexes IndexCache 20 | 21 | // resolved column definitions 22 | Definitions map[string]*Definition 23 | } 24 | 25 | // IsEmpty checks if columns are available 26 | func (s *State) IsEmpty() bool { 27 | return len(s.Columns) < 1 28 | } 29 | 30 | // Index retrieves the index of column 31 | func (s *State) Index(name string) int { 32 | idx, ok := s.Indexes[name] 33 | if ok { 34 | return idx 35 | } 36 | return -1 37 | } 38 | 39 | // HasColumn checks if column is available 40 | func (s *State) HasColumn(name string) bool { 41 | return s.Index(name) > -1 42 | } 43 | 44 | // ColumnName retrieves the name of column from index 45 | func (s *State) ColumnName(idx int) string { 46 | if s.IsEmpty() || idx >= len(s.Columns) { 47 | return "" 48 | } 49 | 50 | return s.Columns[idx] 51 | } 52 | 53 | // HasDefinition checks if definition is available 54 | func (s *State) HasDefinition(name string) bool { 55 | _, ok := s.Definitions[name] 56 | return ok 57 | } 58 | 59 | // Definition retrieves the value definition 60 | func (s *State) Definition(name string) *Definition { 61 | return s.Definitions[name] 62 | } 63 | 64 | // DefinitionAt retrieves the value definition via index 65 | func (s *State) DefinitionAt(idx int) *Definition { 66 | if idx >= len(s.Columns) { 67 | return nil 68 | } 69 | 70 | return s.Definitions[s.Columns[idx]] 71 | } 72 | 73 | // DataTypes returns the saved data types in definitions 74 | func (s *State) DataTypes() map[string]reflect.Type { 75 | return SliceKeyMap(s.Columns, func(col string, _ int) (string, reflect.Type) { 76 | return col, s.DataType(col) 77 | }) 78 | } 79 | 80 | // DataType returns data type for single value 81 | func (s *State) DataType(name string) reflect.Type { 82 | def := s.Definition(name) 83 | if def != nil { 84 | return def.Type 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // Clone duplicates state to new instance 91 | func (s *State) Clone() *State { 92 | return &State{ 93 | Columns: slices.Clone(s.Columns), 94 | Indexes: maps.Clone(s.Indexes), 95 | Definitions: maps.Clone(s.Definitions), 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /table.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | type Table struct { 4 | 5 | // internal state to mark data types are resolved 6 | resolved bool 7 | 8 | // slice of table rows 9 | Rows []*Row 10 | 11 | // current state of table 12 | State *State 13 | 14 | // current resolved options for table 15 | Options *Options 16 | } 17 | 18 | // New creates new [Table] instance with [OptionCallback] 19 | func New(ocbs ...OptionCallback) *Table { 20 | return (&Table{ 21 | resolved: false, 22 | Rows: make([]*Row, 0), 23 | Options: NewOptions(ocbs...), 24 | State: &State{ 25 | Indexes: make(IndexCache), 26 | Columns: make([]string, 0), 27 | Definitions: make(map[string]*Definition, 0), 28 | }, 29 | }).Initialize() 30 | } 31 | -------------------------------------------------------------------------------- /table_access.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | // Length return number of total rows 4 | func (t *Table) Length() int { 5 | return len(t.Rows) 6 | } 7 | 8 | // ColLength returns number of columns 9 | func (t *Table) ColLength() int { 10 | return len(t.State.Columns) 11 | } 12 | 13 | // IsEmpty verifies if table is empty 14 | func (t *Table) IsEmpty() bool { 15 | return t.Length() == 0 || t.ColLength() == 0 16 | } 17 | 18 | // At retrieves row at x index 19 | func (t *Table) At(idx int) *Row { 20 | return t.Rows[idx] 21 | } 22 | 23 | // IsAtMaxLine checks rows are already at restricted max limit 24 | func (t *Table) IsAtMaxLine() bool { 25 | if t.Options.MaxRows < 0 { 26 | return false 27 | } 28 | 29 | return t.Length() >= t.Options.MaxRows 30 | } 31 | 32 | // IsResolved checks table is resolved 33 | func (t *Table) IsResolved() bool { 34 | return t.resolved 35 | } 36 | -------------------------------------------------------------------------------- /table_action.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | // PipelineAction defines an action interface to modify 4 | // table rows and options as needed. 5 | type PipelineAction interface { 6 | 7 | // ActionName returns the name of pipeline action 8 | ActionName() string 9 | 10 | // Execute executes the pipeline action 11 | Execute(*Table) (*Table, error) 12 | } 13 | 14 | // Execute is called by [Table] when pipeline actions are executed 15 | func (t *Table) Execute(actions ...PipelineAction) (*Table, error) { 16 | df := t 17 | 18 | var err error 19 | for _, action := range actions { 20 | df, err = action.Execute(df) 21 | if err != nil { 22 | return nil, err 23 | } 24 | } 25 | 26 | return df, nil 27 | } 28 | 29 | // ExecuteS runs pipeline actions and dereferences the resulting [Table] back to self 30 | func (t *Table) ExecuteS(actions ...PipelineAction) error { 31 | df, err := t.Execute(actions...) 32 | 33 | if err != nil { 34 | return err 35 | } 36 | 37 | *t = *df 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /table_clone.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | // Clone duplicates table as-is 4 | func (t *Table) Clone() *Table { 5 | df := t.CloneE() 6 | df.Rows = append(df.Rows, t.Rows...) 7 | return df 8 | } 9 | 10 | // CloneE duplicates table as-is without rows 11 | func (t *Table) CloneE() *Table { 12 | return &Table{ 13 | resolved: t.resolved, 14 | Rows: make([]*Row, 0), 15 | State: t.State.Clone(), 16 | Options: t.Options.Clone(), 17 | } 18 | } 19 | 20 | // CherryPick selects the columns and assigns new 21 | // rows for those columns to build new [Table] 22 | func CherryPick(src *Table, columns []string, rows []*Row) (*Table, error) { 23 | df := src.CloneE().MarkUnresolved() 24 | 25 | if !src.State.IsEmpty() { 26 | df.UseColumns(SlicePick(src.State.Columns, columns)) 27 | 28 | for _, fRow := range rows { 29 | nRow, err := fRow.CloneP(src.State, columns...) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | df.AddRow(nRow) 35 | } 36 | } 37 | 38 | return df, nil 39 | } 40 | -------------------------------------------------------------------------------- /table_clone_test.go: -------------------------------------------------------------------------------- 1 | package framed_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestTableClone(t *testing.T) { 9 | df := SampleTestTable() 10 | cloned := df.Clone() 11 | 12 | if !reflect.DeepEqual(df, cloned) { 13 | t.Error("unable to produce identical clone of table") 14 | } 15 | } 16 | 17 | func TestTableCloneOptionsDefinitions(t *testing.T) { 18 | df := SampleTestTable() 19 | cloned := df.CloneE().MarkUnresolved() 20 | 21 | if !reflect.DeepEqual(df.State, cloned.State) { 22 | t.Error("unable to produce identical clone state of table") 23 | } 24 | if !reflect.DeepEqual(df.Options, cloned.Options) { 25 | t.Error("unable to produce identical clone options of table") 26 | } 27 | if cloned.IsResolved() || reflect.DeepEqual(df.Rows, cloned.Rows) { 28 | t.Error("unable to detect empty rows in table") 29 | } 30 | } 31 | 32 | func TestTableCloneWithoutRows(t *testing.T) { 33 | df := SampleTestTable() 34 | cloned := df.CloneE() 35 | 36 | if !reflect.DeepEqual(df.State, cloned.State) { 37 | t.Error("unable to produce identical clone state of table") 38 | } 39 | if !reflect.DeepEqual(df.Options, cloned.Options) { 40 | t.Error("unable to produce identical clone options of table") 41 | } 42 | if !cloned.IsResolved() || reflect.DeepEqual(df.Rows, cloned.Rows) { 43 | t.Error("unable to detect empty rows in table") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /table_display.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | // String generates printable stats about the table 9 | func (t *Table) String() string { 10 | rows := t.Length() 11 | sampleSize := t.Options.SampleSize 12 | previewSize := min(t.Options.Sampling, rows) 13 | 14 | buf := bytes.NewBufferString("\n") 15 | 16 | buf.WriteString(fmt.Sprintf("\nColumns (%d):\n------------", t.ColLength())) 17 | for idx, col := range t.State.Columns { 18 | def := t.State.Definition(col) 19 | samples := make([]string, previewSize) 20 | for sI := range previewSize { 21 | sVal, err := ColumnValueEncoder(def, t.At(sI).At(idx)) 22 | if err == nil { 23 | if len(sVal) > sampleSize { 24 | samples[sI] = sVal[0:sampleSize] + "..." 25 | } else { 26 | samples[sI] = sVal 27 | } 28 | } 29 | } 30 | 31 | samplesStr := "" 32 | if previewSize > 0 { 33 | samplesStr = fmt.Sprintf(" = %s", JoinAtChar(samples, t.Options.Separator)) 34 | } 35 | 36 | buf.WriteString(fmt.Sprintf("\n#%d %s (%s)%s", idx, col, t.State.DataType(col), samplesStr)) 37 | } 38 | 39 | buf.WriteString(fmt.Sprintf("\n\nTotal rows: %d\n", rows)) 40 | 41 | return buf.String() 42 | } 43 | -------------------------------------------------------------------------------- /table_input.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | // File opens file and creates table 10 | func File(path string, cbs ...OptionCallback) (*Table, error) { 11 | file, err := os.Open(path) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | return Reader(file, cbs...) 17 | } 18 | 19 | // URL sends http request to url and creates table 20 | func URL(uri string, cbs ...OptionCallback) (*Table, error) { 21 | response, err := http.Get(uri) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return Reader(response.Body, cbs...) 27 | } 28 | 29 | // Reader iterates through [io.Reader] and creates table 30 | func Reader(r io.Reader, cbs ...OptionCallback) (*Table, error) { 31 | c, ok := r.(io.ReadCloser) 32 | if ok { 33 | defer c.Close() 34 | } 35 | 36 | df := New(cbs...) 37 | 38 | err := df.InsertGenBytes(ReaderToLines(r)) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return df, nil 44 | } 45 | 46 | // Lines imports slices of string to create table 47 | func Lines(lines []string, cbs ...OptionCallback) (*Table, error) { 48 | df := New(cbs...) 49 | 50 | err := df.InsertLines(lines) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return df, nil 56 | } 57 | 58 | // Raw imports slices of raw data to create table 59 | func Raw(ss [][]string, cbs ...OptionCallback) (*Table, error) { 60 | df := New(cbs...) 61 | 62 | err := df.InsertSlices(ss) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return df, nil 68 | } 69 | -------------------------------------------------------------------------------- /table_output.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | // Write writes table data to [io.Writer] 9 | func (t *Table) Write(f io.Writer) error { 10 | _, err := f.Write([]byte(JoinAtChar(t.State.Columns, t.Options.Separator))) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | for _, r := range t.Rows { 16 | line, err := r.AsSlice(t.State) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | _, err = f.Write([]byte("\n" + JoinAtChar(line, t.Options.Separator))) 22 | if err != nil { 23 | return err 24 | } 25 | } 26 | 27 | return nil 28 | } 29 | 30 | // Save saves the table data to provided file path 31 | func (t *Table) Save(path string) error { 32 | f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 33 | if err != nil { 34 | return err 35 | } 36 | defer f.Close() 37 | 38 | err = t.Write(f) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /table_output_test.go: -------------------------------------------------------------------------------- 1 | package framed_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | ) 8 | 9 | func TestTableToWriter(t *testing.T) { 10 | df := SampleTestTable() 11 | 12 | var b bytes.Buffer 13 | bWriter := io.Writer(&b) 14 | 15 | err := df.Write(bWriter) 16 | if err != nil { 17 | t.Error(err) 18 | } else if b.Len() == 0 { 19 | t.Error("failed to write table to writer") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /table_resolve.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "slices" 7 | ) 8 | 9 | // Initialize executes the options on table instance 10 | func (t *Table) Initialize() *Table { 11 | if t.Options.Columns != nil { 12 | t.UseColumns(t.Options.Columns) 13 | } 14 | 15 | for n, def := range t.Options.Definitions { 16 | _, ok := t.State.Definitions[n] 17 | if !ok { 18 | t.SetDefinition(n, def) 19 | } 20 | } 21 | 22 | return t 23 | } 24 | 25 | // SetState overrides the [State] of table 26 | func (t *Table) SetState(s *State) *Table { 27 | t.State = s 28 | return t 29 | } 30 | 31 | // SetOptions overrides the [Options] of table 32 | func (t *Table) SetOptions(opts *Options) *Table { 33 | t.Options = opts 34 | return t 35 | } 36 | 37 | // SetColumns updates the header columns for table 38 | func (t *Table) SetColumns(cols []string) { 39 | t.State.Columns = cols 40 | } 41 | 42 | // SetIndexes updates the column indexes for table 43 | func (t *Table) SetIndexes(cache IndexCache) { 44 | t.State.Indexes = cache 45 | } 46 | 47 | // SetDefinition assigns [Definition] for the column 48 | func (t *Table) SetDefinition(name string, def *Definition) *Definition { 49 | t.State.Definitions[name] = def 50 | return def 51 | } 52 | 53 | // ResolveDefinition stores the column [Definition] if it doesn't exist 54 | func (t *Table) ResolveDefinition(name string, tp reflect.Type) *Definition { 55 | def := t.State.Definition(name) 56 | if def != nil { 57 | return def 58 | } 59 | 60 | t.State.Definitions[name] = NewDefinition(tp) 61 | 62 | return t.State.Definition(name) 63 | } 64 | 65 | // ResolveValueDefinition detects data type of column value and creates [Definition] 66 | func (t *Table) ResolveValueDefinition(idx int, name string, value string) *Definition { 67 | def := t.State.Definition(name) 68 | if def != nil { 69 | return def 70 | } 71 | 72 | tp := ToType("") 73 | dContinue := true 74 | if t.Options.TypeReader != nil { 75 | rTp := t.Options.TypeReader(idx, value) 76 | if rTp != nil { 77 | tp = rTp 78 | dContinue = false 79 | } 80 | } 81 | 82 | if dContinue { 83 | tp = DetectValueType(name, value) 84 | } 85 | 86 | t.State.Definitions[name] = NewDefinition(tp) 87 | 88 | return t.State.Definition(name) 89 | } 90 | 91 | // ResolveTypes resolves the data types from the column values 92 | func (t *Table) ResolveTypes(names []string, values []string) error { 93 | if !t.resolved { 94 | t.resolved = true 95 | 96 | if len(names) != len(values) { 97 | return fmt.Errorf("invalid size of column names and values; %d != %d", len(names), len(values)) 98 | } 99 | 100 | for idx, value := range values { 101 | t.ResolveValueDefinition(idx, names[idx], value) 102 | } 103 | } 104 | 105 | return nil 106 | } 107 | 108 | // UseColumns updates header columns for table 109 | func (t *Table) UseColumns(values []string) { 110 | cache := make(IndexCache) 111 | for idx, col := range values { 112 | cache[col] = idx 113 | } 114 | 115 | t.SetColumns(slices.Clone(values)) 116 | t.SetIndexes(cache) 117 | } 118 | 119 | // MarkUnresolved marks table as unresolved 120 | func (t *Table) MarkUnresolved() *Table { 121 | t.resolved = false 122 | return t 123 | } 124 | -------------------------------------------------------------------------------- /table_resolve_test.go: -------------------------------------------------------------------------------- 1 | package framed_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestTableResolveDefinition(t *testing.T) { 9 | df := SampleTestTable() 10 | def := df.ResolveValueDefinition(20, "test", "2.501") 11 | 12 | if def.Kind() != reflect.Float64 { 13 | t.Error("failed to resolve valid data type") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /table_slice.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "iter" 5 | "slices" 6 | ) 7 | 8 | // Slice retrieves slice of rows at given start and end 9 | func (t *Table) Slice(start int, end int) []*Row { 10 | return t.Rows[start:end] 11 | } 12 | 13 | // RSlice retrieves slice of rows at given start and end but reverse 14 | func (t *Table) RSlice(start int, end int) []*Row { 15 | rows := make([]*Row, t.Length()) 16 | _ = copy(rows, t.Rows) 17 | slices.Reverse(rows) 18 | 19 | return rows[start:end] 20 | } 21 | 22 | // First returns the first [Row] of table 23 | func (t *Table) First() *Row { 24 | return t.At(0) 25 | } 26 | 27 | // Last returns the last [Row] of table 28 | func (t *Table) Last() *Row { 29 | return t.At(t.Length() - 1) 30 | } 31 | 32 | // Head returns the first n [Row] of table 33 | func (t *Table) Head(limit int) []*Row { 34 | return t.Slice(0, limit) 35 | } 36 | 37 | // Tail returns the last n [Row] of table 38 | func (t *Table) Tail(limit int) []*Row { 39 | return t.RSlice(0, limit) 40 | } 41 | 42 | // Chunk build new table from provided slice indexes 43 | func (t *Table) Chunk(start int, end int) *Table { 44 | df := t.CloneE() 45 | rows := t.Slice(start, end) 46 | 47 | for idx, r := range rows { 48 | df.AddRow(r.Clone().WithIndex(idx)) 49 | } 50 | 51 | return df 52 | } 53 | 54 | // Chunks split table into multiple chunk of tables from provided slice indexes 55 | func (t *Table) Chunks(limit int) iter.Seq2[int, *Table] { 56 | return func(yield func(int, *Table) bool) { 57 | count := t.Length() 58 | pages := count / limit 59 | if count > (pages * limit) { 60 | pages++ 61 | } 62 | 63 | for i := range pages { 64 | start := i * limit 65 | end := min(start+limit, count) 66 | 67 | if !yield(i, t.Chunk(start, end)) { 68 | return 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /table_test.go: -------------------------------------------------------------------------------- 1 | package framed_test 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "fmt" 8 | 9 | "github.com/OpenRunic/framed" 10 | ) 11 | 12 | const stringSet = "abcdefghijklmnopqrstuvwxyz" + 13 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 14 | 15 | var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) 16 | 17 | func StringRandom(length int) string { 18 | b := make([]byte, length) 19 | for i := range b { 20 | b[i] = stringSet[seededRand.Intn(len(stringSet))] 21 | } 22 | return string(b) 23 | } 24 | 25 | func SampleTestTable(sizes ...int) *framed.Table { 26 | size := 100 27 | if len(sizes) > 0 { 28 | size = sizes[0] 29 | } 30 | 31 | rows := make([][]string, size) 32 | for idx := range size { 33 | rows[idx] = []string{ 34 | fmt.Sprint(idx + 1), StringRandom(10), StringRandom(10), fmt.Sprint(rand.Intn(50)), 35 | } 36 | } 37 | 38 | df, _ := framed.Raw(rows, 39 | framed.WithColumns( 40 | "id", "first_name", "last_name", "age", 41 | ), 42 | ) 43 | 44 | return df 45 | } 46 | -------------------------------------------------------------------------------- /table_write.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "io" 5 | "iter" 6 | ) 7 | 8 | // Read accepts [io.Reader] to loads data into table 9 | func (t *Table) Read(r io.Reader) error { 10 | var n int 11 | var err error 12 | buf := make([]byte, 256) 13 | 14 | for err == nil { 15 | n, err = r.Read(buf) 16 | if err != nil { 17 | if err != io.EOF { 18 | return err 19 | } 20 | } else { 21 | if t.IsAtMaxLine() { 22 | break 23 | } 24 | 25 | err := t.Insert(buf[:n]) 26 | if err != nil { 27 | return err 28 | } 29 | } 30 | } 31 | 32 | return nil 33 | } 34 | 35 | // SliceToColumns convert slice of values to column slices 36 | func (t *Table) SliceToColumns(values []string) ([]any, error) { 37 | hLen := t.ColLength() 38 | columns := make([]any, len(values)) 39 | 40 | for idx, value := range values { 41 | if hLen > idx { 42 | c, err := t.AsColumn(idx, value) 43 | if err != nil { 44 | return nil, err 45 | } 46 | columns[idx] = c 47 | } 48 | } 49 | 50 | return columns, nil 51 | } 52 | 53 | // AsColumn builds column info for provided value 54 | func (t *Table) AsColumn(idx int, value string) (any, error) { 55 | def := t.State.Definition(t.State.ColumnName(idx)) 56 | val, err := ColumnValueDecoder(def, value) 57 | if err != nil { 58 | return nil, ColError(t.Length()-1, idx, t.State.ColumnName(idx), err, "value_decode") 59 | } 60 | return val, nil 61 | } 62 | 63 | func (t *Table) AddRow(rows ...*Row) { 64 | t.Rows = append(t.Rows, rows...) 65 | } 66 | 67 | // AppendColumn adds new column to table 68 | func (t *Table) AppendColumn(pos int, name string) { 69 | t.State.Indexes[name] = pos 70 | t.State.Columns = append(t.State.Columns, name) 71 | } 72 | 73 | // Insert adds line of bytes as row 74 | func (t *Table) Insert(b []byte) error { 75 | return t.InsertLine(string(b)) 76 | } 77 | 78 | // InsertLine adds string line as row 79 | func (t *Table) InsertLine(line string) error { 80 | return t.InsertSlice(SplitAtChar(line, t.Options.Separator)) 81 | } 82 | 83 | // InsertSlice adds slice of strings as row 84 | func (t *Table) InsertSlice(values []string) error { 85 | line := t.Length() 86 | if line == 0 { 87 | if t.Options.IgnoreHeader { 88 | return nil 89 | } 90 | 91 | if t.State.IsEmpty() { 92 | t.UseColumns(values) 93 | return nil 94 | } 95 | } 96 | 97 | if !t.resolved { 98 | err := t.ResolveTypes(t.State.Columns, values) 99 | if err != nil { 100 | return err 101 | } 102 | } 103 | 104 | columns, err := t.SliceToColumns(values) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | t.AddRow(&Row{ 110 | Index: line, 111 | Columns: columns, 112 | }) 113 | 114 | return nil 115 | } 116 | 117 | // InsertLines iterates list of string lines 118 | func (t *Table) InsertLines(lines []string) error { 119 | for _, line := range lines { 120 | if t.IsAtMaxLine() { 121 | break 122 | } 123 | 124 | err := t.InsertLine(line) 125 | if err != nil { 126 | return err 127 | } 128 | } 129 | 130 | return nil 131 | } 132 | 133 | // InsertSlices iterates list of string slice 134 | func (t *Table) InsertSlices(ss [][]string) error { 135 | for _, s := range ss { 136 | if t.IsAtMaxLine() { 137 | break 138 | } 139 | 140 | err := t.InsertSlice(s) 141 | if err != nil { 142 | return err 143 | } 144 | } 145 | 146 | return nil 147 | } 148 | 149 | // InsertGenBytes iterates bytes from iterator 150 | func (t *Table) InsertGenBytes(it iter.Seq[[]byte]) error { 151 | for b := range it { 152 | if t.IsAtMaxLine() { 153 | break 154 | } 155 | 156 | err := t.InsertLine(string(b)) 157 | if err != nil { 158 | return err 159 | } 160 | } 161 | 162 | return nil 163 | } 164 | -------------------------------------------------------------------------------- /typecast.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "regexp" 7 | "slices" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // BoolToStringValues contains slice of string 13 | // values that can be auto-translated to boolean 14 | var BoolToStringValues = []string{"1", "false", "no", "n"} 15 | 16 | // BoolTrueStringValues contains slice of string 17 | // value that can be auto-translated to true boolean 18 | var BoolTrueStringValues = []string{"1", "true", "yes", "y"} 19 | 20 | // StringTranslatableKinds contains slice of [reflect.Kind] 21 | // that can be translated to string 22 | var StringTranslatableKinds = []reflect.Kind{ 23 | reflect.String, 24 | reflect.Int, reflect.Int32, reflect.Int64, 25 | reflect.Float32, reflect.Float64, 26 | reflect.Bool, 27 | } 28 | 29 | func init() { 30 | copy(BoolToStringValues, BoolTrueStringValues) 31 | } 32 | 33 | // ToType reads the [reflect.Type] from provided value 34 | func ToType[T any](v T) reflect.Type { 35 | tp := reflect.TypeOf(v) 36 | if tp.Kind() == reflect.Slice { 37 | return tp.Elem() 38 | } 39 | return tp 40 | } 41 | 42 | // DetectValueType detects and returns [reflect.Type] of string value 43 | func DetectValueType(name string, value string) reflect.Type { 44 | sLen := len(name) 45 | sName := strings.ToLower(name) 46 | if slices.Contains([]string{"id"}, sName) || (sLen > 3 && name[len(sName)-3:] == "_id") { 47 | return ToType(0) 48 | } 49 | 50 | if slices.Contains(BoolToStringValues, strings.ToLower(value)) { 51 | return ToType(false) 52 | } 53 | 54 | matched, _ := regexp.MatchString("^[0-9]+$", value) 55 | if matched { 56 | _, err := strconv.Atoi(value) 57 | if err == nil { 58 | return ToType[int64](0) 59 | } 60 | } 61 | 62 | matched, _ = regexp.MatchString("^[0-9.]+$", value) 63 | if matched { 64 | _, err := strconv.ParseFloat(value, 64) 65 | if err == nil { 66 | return ToType(0.1) 67 | } 68 | } 69 | 70 | return ToType("") 71 | } 72 | 73 | // ParseInt converts string to int as base10 with variable bit size 74 | func ParseInt(s string, bitSize int) (int64, error) { 75 | val, err := strconv.ParseInt(s, 10, bitSize) 76 | if err != nil { 77 | return 0, err 78 | } 79 | return val, nil 80 | } 81 | 82 | // ConvertValueType converts string to provided type or throws error 83 | func ConvertValueType(value string, tp reflect.Type) (any, error) { 84 | kind := tp.Kind() 85 | switch kind { 86 | case reflect.Bool: 87 | return slices.Contains(BoolTrueStringValues, strings.ToLower(value)), nil 88 | 89 | case reflect.Int: 90 | val, err := strconv.Atoi(value) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | return val, nil 96 | 97 | case reflect.Int32: 98 | val, err := ParseInt(value, 32) 99 | if err != nil { 100 | return nil, err 101 | } 102 | return int32(val), nil 103 | 104 | case reflect.Int64: 105 | val, err := ParseInt(value, 64) 106 | if err != nil { 107 | return nil, err 108 | } 109 | return val, nil 110 | 111 | case reflect.Float32: 112 | val, err := strconv.ParseFloat(value, 32) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | return float32(val), nil 118 | 119 | case reflect.Float64: 120 | val, err := strconv.ParseFloat(value, 64) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | return val, nil 126 | } 127 | 128 | if len(value) < 1 { 129 | return nil, nil 130 | } 131 | 132 | return value, nil 133 | } 134 | 135 | // ColumnValueEncoder encodes the column value to string 136 | // using definition or with default methods 137 | func ColumnValueEncoder(def *Definition, value any) (string, error) { 138 | if def.Encoder != nil { 139 | return def.Encoder(value) 140 | } 141 | 142 | if def.Kind() == reflect.String || slices.Contains(StringTranslatableKinds, def.Kind()) { 143 | return fmt.Sprintf("%v", value), nil 144 | } 145 | 146 | return "", fmt.Errorf("failed to encode %s value to string", def.Type) 147 | } 148 | 149 | // ColumnValueDecoder decodes the column value from string 150 | // using column definition or with default methods 151 | func ColumnValueDecoder(def *Definition, value string) (any, error) { 152 | if def.Decoder != nil { 153 | return def.Decoder(value) 154 | } 155 | 156 | if def.Kind() == reflect.String { 157 | return value, nil 158 | } else if slices.Contains(StringTranslatableKinds, def.Kind()) { 159 | val, err := ConvertValueType(value, def.Type) 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | return val, nil 165 | } 166 | 167 | return nil, fmt.Errorf("failed to decode value to %s", def.Type) 168 | } 169 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package framed 2 | 3 | import ( 4 | "slices" 5 | ) 6 | 7 | // SliceMap converts slices of T to slices of K 8 | func SliceMap[T any, K any](src []T, cb func(T) K) []K { 9 | result := make([]K, len(src)) 10 | 11 | for idx, item := range src { 12 | result[idx] = cb(item) 13 | } 14 | 15 | return result 16 | } 17 | 18 | // SliceKeyMap convert slices of T to map of L[M] 19 | func SliceKeyMap[T any, L comparable, M any](src []T, cb func(T, int) (L, M)) map[L]M { 20 | result := make(map[L]M) 21 | 22 | for idx, item := range src { 23 | key, value := cb(item, idx) 24 | result[key] = value 25 | } 26 | 27 | return result 28 | } 29 | 30 | // SliceFilter filters slice values based on filter func 31 | func SliceFilter[T any](src []T, cb func(T) bool) []T { 32 | result := make([]T, 0) 33 | 34 | for _, item := range src { 35 | if cb(item) { 36 | result = append(result, item) 37 | } 38 | } 39 | 40 | return result 41 | } 42 | 43 | // SlicePick picks slice values 44 | func SlicePick[T comparable](src []T, keys []T) []T { 45 | return SliceFilter(src, func(t T) bool { 46 | return slices.Contains(keys, t) 47 | }) 48 | } 49 | 50 | // SlicePick omits slice values 51 | func SliceOmit[T comparable](src []T, keys []T) []T { 52 | return SliceFilter(src, func(t T) bool { 53 | return !slices.Contains(keys, t) 54 | }) 55 | } 56 | --------------------------------------------------------------------------------