Skip to content

Commit

Permalink
Add --include option to display columns selectively
Browse files Browse the repository at this point in the history
Signed-off-by: aswinkarthik <[email protected]>
  • Loading branch information
aswinkarthik committed Feb 16, 2019
1 parent 239708f commit 978ba9d
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 88 deletions.
18 changes: 14 additions & 4 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ func init() {

// Config is to store all command line Flags.
type Config struct {
PrimaryKeyPositions []int
ValueColumnPositions []int
Format string
PrimaryKeyPositions []int
ValueColumnPositions []int
IncludeColumnPositions []int
Format string
}

// GetPrimaryKeys is to return the --primary-key flags as digest.Positions array.
Expand All @@ -28,14 +29,23 @@ func (c *Config) GetPrimaryKeys() digest.Positions {
return []int{0}
}

// GetValueColumns is to return the --value-columns flags as digest.Positions array.
// GetValueColumns is to return the --columns flags as digest.Positions array.
func (c *Config) GetValueColumns() digest.Positions {
if len(c.ValueColumnPositions) > 0 {
return c.ValueColumnPositions
}
return []int{}
}

// GetIncludeColumnPositions is to return the --include flags as digest.Positions array.
// If empty, it is value columns
func (c *Config) GetIncludeColumnPositions() digest.Positions {
if len(c.IncludeColumnPositions) > 0 {
return c.IncludeColumnPositions
}
return c.GetValueColumns()
}

// Validate validates the config object
// and returns error if not valid.
func (c *Config) Validate() error {
Expand Down
56 changes: 14 additions & 42 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ import (
"time"

"github.com/aswinkarthik/csvdiff/pkg/digest"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var (
Expand Down Expand Up @@ -70,8 +68,6 @@ Most suitable for csv files created from database tables`,

return nil
},
// Uncomment the following line if your bare application
// has an action associated with it:
Run: func(cmd *cobra.Command, args []string) {
// Print version and exit program
if version {
Expand All @@ -88,8 +84,18 @@ Most suitable for csv files created from database tables`,
deltaFile := newReadCloser(args[1])
defer deltaFile.Close()

baseConfig := digest.NewConfig(baseFile, config.GetPrimaryKeys(), config.GetValueColumns())
deltaConfig := digest.NewConfig(deltaFile, config.GetPrimaryKeys(), config.GetValueColumns())
baseConfig := digest.NewConfig(
baseFile,
config.GetPrimaryKeys(),
config.GetValueColumns(),
config.GetIncludeColumnPositions(),
)
deltaConfig := digest.NewConfig(
deltaFile,
config.GetPrimaryKeys(),
config.GetValueColumns(),
config.GetIncludeColumnPositions(),
)

diff, err := digest.Diff(baseConfig, deltaConfig)

Expand Down Expand Up @@ -126,57 +132,23 @@ func isValidFile(path string) error {
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
fmt.Fprintf(os.Stderr, "%v", err)
os.Exit(1)
}
}

func init() {
cobra.OnInitialize(initConfig)

// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.csvdiff.yaml)")

// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")

rootCmd.Flags().IntSliceVarP(&config.PrimaryKeyPositions, "primary-key", "p", []int{0}, "Primary key positions of the Input CSV as comma separated values Eg: 1,2")
rootCmd.Flags().IntSliceVarP(&config.ValueColumnPositions, "columns", "", []int{}, "Selectively compare positions in CSV Eg: 1,2. Default is entire row")
rootCmd.Flags().IntSliceVarP(&config.IncludeColumnPositions, "include", "", []int{}, "Include positions in CSV to display Eg: 1,2. Default is entire row")
rootCmd.Flags().StringVarP(&config.Format, "format", "", "rowmark", "Available (rowmark|json)")

rootCmd.Flags().BoolVarP(&timed, "time", "", false, "Measure time")
rootCmd.Flags().BoolVarP(&version, "version", "", false, "Display version")
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}

// Search config in home directory with name ".csvdiff" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".csvdiff")
}

viper.AutomaticEnv() // read in environment variables that match

// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}

func newReadCloser(filename string) io.ReadCloser {
file, err := os.Open(filename)
if err != nil {
Expand Down
7 changes: 4 additions & 3 deletions pkg/digest/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/csv"
"fmt"
"runtime"
"strings"
"sync"
)

Expand Down Expand Up @@ -100,17 +99,19 @@ func compareDigestForNLines(base map[uint64]uint64,
if baseValue, present := base[digest.Key]; present {
// Present in both base and delta
if baseValue != digest.Value {
value := config.Include.MapToValue(line)
// Modification
output[diffCounter] = diffMessage{
value: strings.Join(line, Separator),
value: value,
_type: modification,
}
diffCounter++
}
} else {
value := config.Include.MapToValue(line)
// Not present in base. So Addition.
output[diffCounter] = diffMessage{
value: strings.Join(line, Separator),
value: value,
_type: addition,
}
diffCounter++
Expand Down
117 changes: 90 additions & 27 deletions pkg/digest/compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,97 @@ func TestDiff(t *testing.T) {
delta := `1,col-1,col-2,col-3,one-value
2,col-1,col-2,col-3,two-value-modified
4,col-1,col-2,col-3,four-value-added
100,col-1,col-2,col-3,hundred-value-modified
100,col-1-modified,col-2,col-3,hundred-value-modified
5,col-1,col-2,col-3,five-value-added
`

baseConfig := &digest.Config{
Reader: strings.NewReader(base),
Key: []int{0},
}

deltaConfig := &digest.Config{
Reader: strings.NewReader(delta),
Key: []int{0},
}

expected := digest.Difference{
Additions: []string{
"4,col-1,col-2,col-3,four-value-added",
"5,col-1,col-2,col-3,five-value-added",
},
Modifications: []string{
"2,col-1,col-2,col-3,two-value-modified",
"100,col-1,col-2,col-3,hundred-value-modified",
},
}

actual, err := digest.Diff(baseConfig, deltaConfig)

assert.NoError(t, err)
assert.ElementsMatch(t, expected.Modifications, actual.Modifications)
assert.ElementsMatch(t, expected.Additions, actual.Additions)
t.Run("default config", func(t *testing.T) {
baseConfig := &digest.Config{
Reader: strings.NewReader(base),
Key: []int{0},
}

deltaConfig := &digest.Config{
Reader: strings.NewReader(delta),
Key: []int{0},
}

expected := digest.Difference{
Additions: []string{
"4,col-1,col-2,col-3,four-value-added",
"5,col-1,col-2,col-3,five-value-added",
},
Modifications: []string{
"2,col-1,col-2,col-3,two-value-modified",
"100,col-1-modified,col-2,col-3,hundred-value-modified",
},
}

actual, err := digest.Diff(baseConfig, deltaConfig)

assert.NoError(t, err)
assert.ElementsMatch(t, expected.Modifications, actual.Modifications)
assert.ElementsMatch(t, expected.Additions, actual.Additions)
})

t.Run("selective values columns", func(t *testing.T) {
baseConfig := &digest.Config{
Reader: strings.NewReader(base),
Key: []int{0},
Value: []int{1},
}

deltaConfig := &digest.Config{
Reader: strings.NewReader(delta),
Key: []int{0},
Value: []int{1},
}

expected := digest.Difference{
Additions: []string{
"4,col-1,col-2,col-3,four-value-added",
"5,col-1,col-2,col-3,five-value-added",
},
Modifications: []string{
"100,col-1-modified,col-2,col-3,hundred-value-modified",
},
}

actual, err := digest.Diff(baseConfig, deltaConfig)

assert.NoError(t, err)
assert.ElementsMatch(t, expected.Modifications, actual.Modifications)
assert.ElementsMatch(t, expected.Additions, actual.Additions)
})

t.Run("selective include columns", func(t *testing.T) {
baseConfig := &digest.Config{
Reader: strings.NewReader(base),
Key: []int{0},
Include: []int{0},
}

deltaConfig := &digest.Config{
Reader: strings.NewReader(delta),
Key: []int{0},
Include: []int{0},
}

expected := digest.Difference{
Additions: []string{
"4",
"5",
},
Modifications: []string{
"2",
"100",
},
}

actual, err := digest.Diff(baseConfig, deltaConfig)

assert.NoError(t, err)
assert.ElementsMatch(t, expected.Modifications, actual.Modifications)
assert.ElementsMatch(t, expected.Additions, actual.Additions)
})
}
24 changes: 17 additions & 7 deletions pkg/digest/digest.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,28 @@ func CreateDigest(csv []string, pKey Positions, pRow Positions) Digest {

// Config represents configurations that can be passed
// to create a Digest.
//
// Key: The primary key positions
// Value: The Value positions that needs to be compared for diff
// Include: Include these positions in output. It is Value positions by default.
type Config struct {
Key Positions
Value Positions
Reader io.Reader
Key Positions
Value Positions
Include Positions
Reader io.Reader
}

// NewConfig creates an instance of Config struct.
func NewConfig(r io.Reader, primaryKey Positions, valueColumns Positions) *Config {
func NewConfig(r io.Reader, primaryKey Positions, valueColumns Positions, includeColumns Positions) *Config {
if len(includeColumns) == 0 {
includeColumns = valueColumns
}

return &Config{
Reader: r,
Key: primaryKey,
Value: valueColumns,
Reader: r,
Key: primaryKey,
Value: valueColumns,
Include: includeColumns,
}
}

Expand Down
38 changes: 33 additions & 5 deletions pkg/digest/positions.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package digest

import "strings"
import (
"strings"
)

// Positions represents positions of columns in a CSV array.
type Positions []int
Expand All @@ -12,9 +14,35 @@ func (p Positions) MapToValue(csv []string) string {
if len(p) == 0 {
return strings.Join(csv, Separator)
}
output := make([]string, len(p))
for i, pos := range p {
output[i] = csv[pos]

csvStr := strings.Builder{}
for _, pos := range p[:len(p)-1] {
csvStr.WriteString(csv[pos])
csvStr.WriteString(Separator)
}
return strings.Join(output, Separator)
csvStr.WriteString(csv[p[len(p)-1]])
return csvStr.String()
}

// Append additional positions to existing positions.
// Imp: Removes Duplicate. Does not mutate the original array
func (p Positions) Append(additional Positions) Positions {
for _, toBeAdded := range additional {
if !p.Contains(toBeAdded) {
p = append(p, toBeAdded)
}
}

return p
}

// Contains returns true if position is already present in Positions
func (p Positions) Contains(position int) bool {
for _, each := range p {
if each == position {
return true
}
}

return false
}
16 changes: 16 additions & 0 deletions pkg/digest/positions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,19 @@ func TestPositionsMapValuesReturnsCompleteStringCsvIfEmpty(t *testing.T) {

assert.Equal(t, expected, actual)
}

func TestPosition_Contains(t *testing.T) {
positions := digest.Positions([]int{0, 3})

assert.True(t, positions.Contains(3))
assert.False(t, positions.Contains(4))
}

func TestPosition_Append(t *testing.T) {
positions := digest.Positions([]int{0, 3})
additionalPositions := digest.Positions([]int{4, 3})

positions = positions.Append(additionalPositions)

assert.ElementsMatch(t, []int{0, 3, 4}, []int(positions))
}

0 comments on commit 978ba9d

Please sign in to comment.