Skip to content

Commit

Permalink
Merge pull request #56 from pinzolo/numeric
Browse files Browse the repository at this point in the history
Numeric
  • Loading branch information
pinzolo committed Jun 4, 2017
2 parents 553f25f + 2150f6a commit b137f67
Show file tree
Hide file tree
Showing 13 changed files with 788 additions and 9 deletions.
2 changes: 1 addition & 1 deletion cmd/csvutil/cmd_address.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
var cmdAddress = &Command{
Run: runAddress,
UsageLine: "address [OPTIONS...] [FILE]",
Short: "住所出力",
Short: "住所生成",
Long: `DESCRIPTION
指定した列にダミーの住所を出力します。
郵便番号、都道府県、都市、町は同じ列を指定すれば追記されます。
Expand Down
2 changes: 1 addition & 1 deletion cmd/csvutil/cmd_building.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
var cmdBuilding = &Command{
Run: runBuilding,
UsageLine: "building [OPTIONS...] [FILE]",
Short: "建物出力",
Short: "建物生成",
Long: `DESCRIPTION
指定した列にダミーの建物を出力します。
自宅の場合は部屋番号、勤務先の場合はフロアを建物名似合わせて出力します。
Expand Down
2 changes: 1 addition & 1 deletion cmd/csvutil/cmd_convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ var cmdConvert = &Command{
UsageLine: "convert [OPTIONS...] [FILE]",
Short: "形式変換",
Long: `DESCRIPTION
CSVのデータを特定の形式に変換して出力します。出力エンコードは UTF-8 固定です
CSVのデータを特定の形式に変換して標準出力に出力します
ARGUMENTS
FILE
Expand Down
2 changes: 1 addition & 1 deletion cmd/csvutil/cmd_email.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
var cmdEmail = &Command{
Run: runEmail,
UsageLine: "email [OPTIONS...] [FILE]",
Short: "メールアドレス出力",
Short: "メールアドレス生成",
Long: `DESCRIPTION
指定した列にダミーのメールアドレスを出力します。
Expand Down
119 changes: 119 additions & 0 deletions cmd/csvutil/cmd_numeric.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

import (
"github.com/pinzolo/csvutil"
)

var cmdNumeric = &Command{
Run: runNumeric,
UsageLine: "numeric [OPTIONS...] [FILE]",
Short: "数値生成",
Long: `DESCRIPTION
指定した列にmin <= n < max となるランダムな数値を出力します。
ARGUMENTS
FILE
ソースとなる CSV ファイルのパスを指定します。
パスが指定されていない場合、標準入力が対象となりパイプでの使用ができます。
OPTIONS
-w, --overwrite
指定されたCSVファイルを実行結果で上書きします。
ファイルパスが渡されていない場合には無視されます。
-H, --no-header
ソースとなるCSVの1行目をヘッダー列として扱いません。
-b, --backup
処理が成功した場合に、指定されたCSVファイルをバックアップします。
--overwrite オプションと同時に使用されることを想定しているため、ファイルパスが渡されていない場合には無視されます。
-e, --encoding ENCODING
ソースとなるCSVの文字エンコーディングを指定します。
このオプションが指定されていない場合、csvutil はUTF-8とみなして処理を行います。
UTF-8であった場合、BOMのあるなしは自動的に判別されます。
対応している値:
sjis : Shift_JISとして扱います
eucjp: EUC_JPとして扱います
-oe, --output-encoding ENCODING
出力するCSVの文字エンコーディングを指定します。
このオプションが指定されていない場合 --encoding オプションで指定されたエンコーディングとして出力します。
対応している値:
utf8 : UTF-8として出力します(BOMは出力しません)
utf8bom : UTF-8として出力します(BOMは出力します)
sjis : Shift_JISとして出力します
eucjp : EUC_JPとして出力します
-c, --column COLUMN_SYMBOL
対象となる列のシンボルを指定します。
列のシンボルとは列のインデックス(0開始)、もしくはヘッダーテキストです。
--no-header オプションが指定された場合、インデックスしか受け入れません。
-mx, --max NUMBER
出力する数値の最大値を指定します。ただし、ここで指定した値は出力されません。
-mn, --min NUMBER
出力する数値の最小値を指定します。
-d, --decimal
出力する数値を小数として出力します。
-dd, --decimal-digit NUMBER
出力する小数の有効桁数を指定します。値は正の整数でなければいけません。(初期値: 3)
`,
}

type cmdNumericOption struct {
csvutil.NumericOption
Overwrite bool
Backup bool
}

var numericOpt = cmdNumericOption{}

func init() {
cmdNumeric.Flag.BoolVar(&numericOpt.Overwrite, "overwrite", false, "Overwrite to source.")
cmdNumeric.Flag.BoolVar(&numericOpt.Overwrite, "w", false, "Overwrite to source.")
cmdNumeric.Flag.BoolVar(&numericOpt.NoHeader, "no-header", false, "Source file does not have header line.")
cmdNumeric.Flag.BoolVar(&numericOpt.NoHeader, "H", false, "Source file does not have header line.")
cmdNumeric.Flag.BoolVar(&numericOpt.Backup, "backup", false, "Backup source file.")
cmdNumeric.Flag.BoolVar(&numericOpt.Backup, "b", false, "Backup source file.")
cmdNumeric.Flag.StringVar(&numericOpt.Encoding, "encoding", "utf8", "Encoding of source file")
cmdNumeric.Flag.StringVar(&numericOpt.Encoding, "e", "utf8", "Encoding of source file")
cmdNumeric.Flag.StringVar(&numericOpt.OutputEncoding, "output-encoding", "", "Encoding for output")
cmdNumeric.Flag.StringVar(&numericOpt.OutputEncoding, "oe", "", "Encoding for output")
cmdNumeric.Flag.StringVar(&numericOpt.Column, "column", "", "Target column symbol")
cmdNumeric.Flag.StringVar(&numericOpt.Column, "c", "", "Target column symbol")
cmdNumeric.Flag.IntVar(&numericOpt.Max, "max", 100, "Maximum value")
cmdNumeric.Flag.IntVar(&numericOpt.Max, "mx", 100, "Maximum value")
cmdNumeric.Flag.IntVar(&numericOpt.Min, "min", 0, "Minimum value")
cmdNumeric.Flag.IntVar(&numericOpt.Min, "mn", 0, "Minmum value")
cmdNumeric.Flag.BoolVar(&numericOpt.Decimal, "decimal", false, "Output decimal number")
cmdNumeric.Flag.BoolVar(&numericOpt.Decimal, "d", false, "Output decimal number")
cmdNumeric.Flag.IntVar(&numericOpt.DecimalDigit, "decimal-digit", 3, "Decimal digit number")
cmdNumeric.Flag.IntVar(&numericOpt.DecimalDigit, "dd", 3, "Decimal digit number")
}

// runNumeric executes numeric command and return exit code.
func runNumeric(args []string) int {
success := false
w, wf, r, rf, err := prepare(args, numericOpt.Overwrite)
if wf != nil {
defer wf(&success, numericOpt.Backup)
}
if rf != nil {
defer rf()
}
if err != nil {
return handleError(err)
}

err = csvutil.Numeric(r, w, numericOpt.NumericOption)
if err != nil {
return handleError(err)
}

success = true
return 0
}
77 changes: 77 additions & 0 deletions cmd/csvutil/cmd_numeric_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package main

import (
"regexp"
"testing"
)

func Test_runNumeric(t *testing.T) {
numericOpt.Column = "名前"
if c := runNumeric([]string{testFilePath("utf8.csv")}); c != 0 {
t.Errorf("Invalid success exit code: %d", c)
}
numericOpt.Column = ""
}

func Test_runNumericOnNoFile(t *testing.T) {
if c := runNumeric([]string{testFilePath("no-file.csv")}); c == 0 {
t.Errorf("Invalid failed exit code: %d", c)
}
}

func Test_runNumericOnFail(t *testing.T) {
if c := runNumeric([]string{testFilePath("broken.csv")}); c == 0 {
t.Errorf("Invalid failed exit code: %d", c)
}
}

func Test_runNumericOnBackup(t *testing.T) {
f, err := prepareWritingTest()
defer f()
if err != nil {
t.Error(err)
}
numericOpt.Column = "名前"
numericOpt.Overwrite = true
numericOpt.Backup = true
runNumeric([]string{tempFilePath()})
numericOpt.Backup = false
numericOpt.Overwrite = false
numericOpt.Column = ""
if b, err := existsBackup(); err != nil || !b {
t.Errorf("Failed backup")
}
}

func Test_runNumericOnOverwrite(t *testing.T) {
f, err := prepareWritingTest()
defer f()
if err != nil {
t.Error(err)
}
numericOpt.Column = "名前"
numericOpt.Overwrite = true
runNumeric([]string{tempFilePath()})
numericOpt.Overwrite = false
numericOpt.Column = ""
c, err := overwriteContent()
if err != nil {
t.Error(err)
}
if len(c[0]) != 2 {
t.Errorf("Overwrite failed. got %+v", c)
}
if c[0][0] != "名前" || c[0][1] != "個数" {
t.Errorf("Overwrite failed. got %+v", c)
}
pat, err := regexp.Compile("^\\d+$")
if err != nil {
t.Fatal(err)
}
if !pat.MatchString(c[1][0]) || c[1][1] != "1" {
t.Errorf("Overwrite failed. got %+v", c)
}
if !pat.MatchString(c[2][0]) || c[2][1] != "2" {
t.Errorf("Overwrite failed. got %+v", c)
}
}
2 changes: 1 addition & 1 deletion cmd/csvutil/cmd_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
var cmdPassword = &Command{
Run: runPassword,
UsageLine: "password [OPTIONS...] [FILE]",
Short: "パスワード出力",
Short: "パスワード生成",
Long: `DESCRIPTION
指定した列にダミーのパスワードを出力します。
Expand Down
2 changes: 1 addition & 1 deletion cmd/csvutil/cmd_tel.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
var cmdTel = &Command{
Run: runTel,
UsageLine: "tel [OPTIONS...] [FILE]",
Short: "電話番号出力",
Short: "電話番号生成",
Long: `DESCRIPTION
指定した列にダミーの電話番号を出力します。
Expand Down
1 change: 1 addition & 0 deletions cmd/csvutil/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ var commands = []*Command{
cmdHeader,
cmdInsert,
cmdName,
cmdNumeric,
cmdPassword,
cmdRemove,
cmdSize,
Expand Down
1 change: 0 additions & 1 deletion email_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ func TestEmailWithNoHeaderButColumnNotNumber(t *testing.T) {
if err := Email(r, w, o); err == nil {
t.Error("Email with not number column symbol for no header CSV should raise error.")
}

}

func TestEmailWithNegativeMobileRate(t *testing.T) {
Expand Down
119 changes: 119 additions & 0 deletions numeric.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package csvutil

import (
"fmt"
"io"
"math"
"math/rand"
"strconv"

"github.com/pkg/errors"
)

// NumericOption is option holder for Numeric.
type NumericOption struct {
// Source file does not have header line. (default false)
NoHeader bool
// Encoding of source file. (default utf8)
Encoding string
// Encoding for output.
OutputEncoding string
// Target column symbol.
Column string
// Max value
Max int
// Min value
Min int
// Output decimal instead of integer
Decimal bool
// Digit of decimal
DecimalDigit int
}

func (o NumericOption) validate() error {
if o.Column == "" {
return errors.New("no column")
}
if o.NoHeader {
if !isDigit(o.Column) {
return errors.New("not number column symbol")

}
}
if o.Max <= o.Min {
return errors.New("max should be greater than min")
}
if o.Decimal && o.DecimalDigit <= 0 {
return errors.New("decimal digit is not positive")
}
return nil
}

func (o NumericOption) outputEncoding() string {
if o.OutputEncoding != "" {
return o.OutputEncoding
}
return o.Encoding
}

// Numeric overwrite value of given column by random numbers.
func Numeric(r io.Reader, w io.Writer, o NumericOption) error {
if err := o.validate(); err != nil {
return errors.Wrap(err, "invalid option")
}

cr, bom := reader(r, o.Encoding)
cw := writer(w, bom, o.outputEncoding())
defer cw.Flush()

var col *column
csvp := NewCSVProcessor(cr, cw)
if o.NoHeader {
csvp.SetPreBodyRead(func() error {
col = newColumnWithIndex(o.Column, nil)
return col.err
})
} else {
csvp.SetHeaderHanlder(func(hdr []string) ([]string, error) {
col = newColumnWithIndex(o.Column, hdr)
return hdr, col.err
})
}
csvp.SetRecordHandler(func(rec []string) ([]string, error) {
rec[col.index] = fakeNumeric(o)
return rec, nil
})

return csvp.Process()
}

func fakeNumeric(o NumericOption) string {
if o.Decimal {
return fakeDecimal(o)
}
return fakeInteger(o)
}

func fakeDecimal(o NumericOption) string {
coefficient := int(math.Pow10(o.DecimalDigit))
lim := (o.Max - o.Min) * coefficient
n := rand.Intn(lim) + (o.Min * coefficient)
nega := false
if n < 0 {
nega = true
n = n * -1
}
s := fmt.Sprintf("%0"+strconv.Itoa((o.DecimalDigit+1))+"d", n)
l := len(s)
s = s[:l-o.DecimalDigit] + "." + s[l-o.DecimalDigit:]
if nega {
return "-" + s
}
return s
}

func fakeInteger(o NumericOption) string {
lim := o.Max - o.Min
n := rand.Intn(lim) + o.Min
return strconv.Itoa(n)
}

0 comments on commit b137f67

Please sign in to comment.