diff options
author | George Abbott <george@gabbott.dev> | 2023-11-01 22:31:09 +0000 |
---|---|---|
committer | George Abbott <george@gabbott.dev> | 2023-11-01 22:31:09 +0000 |
commit | d7ba131e756057307499fb2c3349c43e8e2fd38c (patch) | |
tree | 4d040e821f32f0c5fb3a4a7eadf57a4eeae5667c |
-rw-r--r-- | file.go | 13 | ||||
-rw-r--r-- | go.mod | 21 | ||||
-rw-r--r-- | input.go | 210 | ||||
-rw-r--r-- | locate.go | 42 | ||||
-rw-r--r-- | log.go | 84 | ||||
-rw-r--r-- | util.go | 175 | ||||
-rw-r--r-- | values.go | 53 |
7 files changed, 598 insertions, 0 deletions
@@ -0,0 +1,13 @@ +package saggytrousers + +import ( + "git.gabbott.dev/george/excelize/v2" + "fmt" +) + +// Append the row `row` to the sheet `sheet` in file `file` at the given +// index `index`. E.g. the index 42 will append the row at $A$42. +func AppendToFile[T any](file *excelize.File, sheet string, row []T, index int) { + axis := fmt.Sprintf("A%d", index) + file.SetSheetRow(sheet, axis, &row) +} @@ -0,0 +1,21 @@ +module saggytrousers + +go 1.19 + +require ( + anyxcelize v0.0.0-00010101000000-000000000000 + git.gabbott.dev/george/excelize/v2 v2.99.1 +) + +require ( + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.3 // indirect + github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect + github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect + golang.org/x/crypto v0.5.0 // indirect + golang.org/x/net v0.5.0 // indirect + golang.org/x/text v0.6.0 // indirect +) + +replace anyxcelize => ../../anyxcelize diff --git a/input.go b/input.go new file mode 100644 index 0000000..6207c07 --- /dev/null +++ b/input.go @@ -0,0 +1,210 @@ +package saggytrousers + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + // "git.gabbott.dev/george/excelize/v2" + xl "anyxcelize" +) + +/* Public Functions */ +/* func GetString(prompt string) string + * func GetInt(prompt string) int + * func GetFloat64(prompt string) float64 + * func SelectSheet(file *xl.File, sheet string) string + * func SelectHeader(f *xl.File, sheet, choice string, exact bool) int + * func UserSelectHeader(f *xl.File, sheet string) ([]string, int) + * func ChooseFromHeader(header []string) int + * func ChooseFromHeaderPrompt(header []string, prompt string) int + * + */ + + func longest(choices []string) int { + l := 0 + for _, v := range choices { + if len(v) > l { + l = len(v) + } + } + return l + } + +// If prompt == "", then prompt with "The options are: \n". +func selectIndexFromArray(choices []string, prompt string, eachOnNewline bool) int { + Log(false, "selectIndexFromArray: choices %v; see below for prompt; eachOnNewline %v\n", choices, eachOnNewline) + length := longest(choices) + Log(true, "%v\n", prompt) + for i, v := range choices { + var separator string + if eachOnNewline { + separator = "\n" + } else if i != 0 && i % 3 == 0 { + separator = "\n\t" + } else { + separator = "\t" + } + fmt.Printf("%s%2d: %-*s", separator, i + 1, length, v) + } + + var answer int + Log(true, "\nWhat is your selection: ") + for { + answer = GetInt("") + Log(false, "selectIndexFromArray: user inputted %v (one-indexed)\n", answer) + if answer < 1 || answer > len(choices) { // one-indexed + Log(true, "Bad selection, try again: ") + continue + } + break + } + + Log(false, "selectIndexFromArray: returning %v (zero-indexed)\n", answer - 1) + return answer - 1 // zero-indexed +} + +func SelectIndexFromArray(choices []string, prompt string, eachOnNewline bool) int { + return selectIndexFromArray(choices, prompt, eachOnNewline) +} + +// Prompt the user with `prompt`, and then return the input. +func GetString(prompt string) string { + var reader = bufio.NewReader(os.Stdin) + + fmt.Printf("%s", prompt) + answer, _ := reader.ReadString('\n') + + Log(false, "GetString returned %v\n", answer) + return answer +} + +// Prompt the user with `prompt`, and then return the integer the user +// enters. If the user does not enter an integer it re-prompts until he does. +func GetInt(prompt string) int { + var reader = bufio.NewReader(os.Stdin) + + fmt.Printf("%s", prompt) + for { + answer, _ := reader.ReadString('\n') + answer = strings.TrimRight(answer, "\n") + answer = strings.TrimRight(answer, " ") + + i, err := strconv.Atoi(answer) + if err != nil { + fmt.Printf("Invalid number, please re-enter: ") + continue + } + + Log(false, "GetInt returned %v\n", i) + return i + } +} + +func GetFloat64(prompt string) float64 { + var reader = bufio.NewReader(os.Stdin) + + fmt.Printf("%s", prompt) + for { + answer, _ := reader.ReadString('\n') + answer = strings.TrimRight(answer, "\n") + answer = strings.TrimRight(answer, " ") + + i, err := strconv.ParseFloat(answer, 64) + if err != nil { + fmt.Printf("Invalid number, please re-enter: ") + continue + } + + return i + } +} + +// Given a list of sheets, prompt the user to select a sheet and return that +// which was selected. +func userSelectSheet(sheets []string) string { + var selected string + + fmt.Println("Please select the sheet that is to be used.") + selectedIdx := selectIndexFromArray(sheets, "", /* each on newline: */ false) + selected = sheets[selectedIdx] + + return selected +} + +// Takes a potentially sheet name, and returns that sheet if it is a valid name +// or prompt the user to select a valid sheet name if it is not. +func SelectSheet(file *xl.File, sheet string) string { + sheetsMap := file.GetSheetMap() + sheets := GetMapValues(sheetsMap) + + // If the passed `sheet` is correct, return it. + if InSlice(sheets, sheet) { + return sheet + } + + // If there is only one sheet in the file, return that. + if len(sheets) == 1 { + // fmt.Printf("Only one sheet: selecting [%s]", sheets[0]) + return sheets[0] + } + + return userSelectSheet(sheets) +} + +// Takes a potential choice from the header row. Returns the index of the +// choice if it is valid, else prompts the user similarly to UserSelectHeader. +func SelectHeader(f *xl.File, sheet, choice string, exact bool) int { + rows, err := f.Rows(sheet) + if err != nil { + fmt.Printf("SelectHeader(sheet: %s, choice: %s, exact: %v: could not get rows to get the header\n", sheet, choice, exact) + os.Exit(-1) + } + rows.Next() + header, err := rows.Columns() + if err != nil { + fmt.Println("SelectHeader: could not get the header.") + os.Exit(-1) + } + + index := Locate(header, choice) + if index != -1 { + return index + } + + // If cannot find, let the user select + fmt.Println("\nPlease select the correct field.") + index = selectIndexFromArray(header, "", /* each on new line: */ false) + + return index +} + +// Have the user select a column from the header, and return both the header +// and the index of the selected value. +func UserSelectHeader(f *xl.File, sheet string) ([]string, int) { + rows, err := f.Rows(sheet) + if err != nil { + fmt.Println("Could not get rows to get the header.") + os.Exit(-1) + } + + rows.Next() + header, err := rows.Columns() + if err != nil { + fmt.Println("Could not get the header.") + os.Exit(-1) + } + + fmt.Println("\nPlease select the correct field.") + index := selectIndexFromArray(header, "Please select the correct field: ", /* each on new line: */ false) + return header, index +} + +func ChooseFromHeader(header []string) int { + return selectIndexFromArray(header, "", /* each on new line: */ false) +} + +func ChooseFromHeaderPrompt(header []string, prompt string) int { + return selectIndexFromArray(header, prompt, /* each on new line: */ false) +} diff --git a/locate.go b/locate.go new file mode 100644 index 0000000..6498c5f --- /dev/null +++ b/locate.go @@ -0,0 +1,42 @@ +package saggytrousers + +import ( + "strings" +) + +// For Locate, LocateExact +func preprocess(s string) string { + s = strings.ToLower(s) + s = strings.ReplaceAll(s, " ", "") + s = strings.ReplaceAll(s, "-", "") + + return s +} + +// This function is needed for SelectHeader. So we put it in saggytrousers, and +// the badtudexo.Locate function calls this. +// For all other types, including []any, please call LocateExact. +func Locate(header []string, needle string) int { + // Perform preprocessing of header and needle + var cneedle string + var cheader []string + + cneedle = preprocess(needle) + + for _, v := range header { + cheader = append(cheader, preprocess(v)) + } + + return LocateExact(cheader, cneedle) +} + +// Returns the index where the needle is found, returns -1 if not found; +// matches exactly. +func LocateExact[T comparable](header []T, needle T) int { + for i := 0; i < len(header); i++ { + if header[i] == needle { + return i + } + } + return -1 +} @@ -0,0 +1,84 @@ +package saggytrousers + +import ( + "fmt" + "os" + "time" +) + +type LogInfo struct { + Fp *os.File + Prog string + Valid bool /* Did the log file open successfully. If not, simply don't log */ +} + +var logInfo LogInfo + +func LogFree() { + err := logInfo.Fp.Close() + if err != nil { + fmt.Printf("That's not great. We can't close the log file!\n") + } +} + +func LogInit(file, prog string) { + logging := true /* are we logging? Set to false on error. */ + fp, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0755) + if err != nil { + fmt.Printf("The log file at [%v] cannot be opened.\n", file) + choices := make([]string, 3) + choices[0] = "Exit Application" + choices[1] = "Continue without Logging" + choices[2] = "View error message" + for true { + v := SelectIndexFromArray(choices, "What would you like to do? ", /* each on new line: */ true) + switch v { + case 0: /* Exit Application */ + os.Exit(-1) + case 1: /* Continue without Logging */ + logging = false /* execution just continues from here */ + break + case 2: /* View error message */ + fmt.Printf("The error message is:\n%v\n", err) + } + } + } + logInfo = LogInfo { + fp, + prog, + logging, + } + +} + +func WriteLogFile(s string) { + /* TODO: implement. This should just write to the log file in logInfo.Fp */ + _, err := logInfo.Fp.WriteString(fmt.Sprintf("[%v] %v -- %v", + time.Now().Format("2006-01-02 15:04:05"), + logInfo.Prog, + s)) + if err != nil { + fmt.Printf("WriteLogFile log failed with %v\n", err) + } +} + +func Log(stdout bool, format string, a ...any) { + s := fmt.Sprintf(format, a...) + + if stdout { + fmt.Printf(s) + } + + // Regardless, we write to the log file. + WriteLogFile(s) +} + +func ErrLog(stderr bool, format string, a ...any) { + s := fmt.Sprintf(format, a...) + + if stderr { + fmt.Printf("[ERROR] %v", s) + } + + WriteLogFile(s) +} @@ -0,0 +1,175 @@ +package saggytrousers + +import ( + "fmt" + "strings" + "errors" + "reflect" + "math" + "strconv" +) + +// Given a map, return all the values as an array. +func GetMapValues[K comparable, V any](m map[K]V) []V { + vs := []V{} + for _, v := range m { + vs = append(vs, v) + } + return vs +} + +// Check if a given value is found in a slice. +func InSlice[T comparable](s []T, v T) bool { + for _, sv := range s { + if sv == v { + return true + } + } + return false +} + +// A concrete instantiation for any since any does not implement comparable. +func InSliceAny(s []any, v any) bool { + for _, sv := range s { + if sv == v { + return true + } + } + return false +} +func SliceEq[T comparable](fst, snd []T) bool { + if len(fst) != len(snd) { + return false + } + + for i := 0; i < len(fst); i++ { + if fst[i] != snd[i] { + return false + } + } + return true +} + + +// Create a filepath, consisting of a directory, a subdirectory within that +// called the `saveDir`, a filename which may contain an instance of `%s`, and +// a key which replaces the %s in the filename. +func CreateFilepath(dir, saveDir, filename, key string) string { + name := CreateFilename(filename, key) + // Add directory + ret := fmt.Sprintf("%s/%s/%s", dir, saveDir, name) + return ret +} + +func CreateFilename(filename, key string) string { + name := fmt.Sprintf(filename, key) + + // Remove illegal characters + name = strings.TrimRight(name, "\n") + name = strings.ReplaceAll(name, " ", "-") + name = strings.ReplaceAll(name, "/", "-") + name = strings.ReplaceAll(name, "\\", "-") + name = strings.ReplaceAll(name, ":", "-") + name = strings.ReplaceAll(name, ";", "-") + name = strings.ReplaceAll(name, "!", "-") + name = strings.ReplaceAll(name, "?", "-") + + return name +} + +// Take input of CreateFilename or something similar, and trim it to 20 for +// making a sheet name. +func MakeValidSheetName(s string) string { + var max int + if len(s) < 20 { + max = len(s) + } else { + max = 20 + } + return s[0:max] +} + +// Downcasts a []any to a []T. Returns error if any fail. +func Downcast[T any](vs []any) ([]T, error) { + var ret []T + for i, v := range vs { + cv, ok := v.(T) + if !ok { + errmesg := fmt.Sprintf("Downcast: failed conv at idx %v for value %v type %T", i, v, v) + return ret, errors.New(errmesg) + } + ret = append(ret, cv) + } + return ret, nil +} + +func DowncastF64(vs []any) ([]float64, error) { + var ret []float64 + for i, v := range vs { + cv, err := ConvertF64(v) + if err != nil { + errmesg := fmt.Sprintf("Downcast: failed conv at idx %v for value %v type %T", i, v, v) + return ret, errors.New(errmesg) + } + ret = append(ret, cv) + } + return ret, nil +} + +// A conversion function to f64 from any. Why this isn't in the standard god +// knows. From StackOverflow question 20767724. +var floatType = reflect.TypeOf(float64(0)) +var stringType = reflect.TypeOf("") +func ConvertF64(unkn any) (float64, error) { + switch i := unkn.(type) { + case float64: + return i, nil + case float32: + return float64(i), nil + case int64: + return float64(i), nil + case int32: + return float64(i), nil + case int: + return float64(i), nil + case uint64: + return float64(i), nil + case uint32: + return float64(i), nil + case uint: + return float64(i), nil + case string: + return strconv.ParseFloat(i, 64) + default: + v := reflect.ValueOf(unkn) + v = reflect.Indirect(v) + if v.Type().ConvertibleTo(floatType) { + fv := v.Convert(floatType) + return fv.Float(), nil + } else if v.Type().ConvertibleTo(stringType) { + sv := v.Convert(stringType) + s := sv.String() + return strconv.ParseFloat(s, 64) + } else { + return math.NaN(), fmt.Errorf("Can't convert %v to float64", v.Type()) + } + } +} + +// Get a given _column_ from a set of rows. This is used to get columns from +// GetRows, which is potentially faster than calling GetColumns. +func TransmogrifyRows(rows [][]any, index int) []any { + var ret []any + for _, row := range rows { + ret = append(ret, row[index]) + } + return ret +} + +// Process a string, removing spaces, making all lowercase, etc. so that we get +// easily `switch`-able strings. +func ProcessStr(str string) string { + ret := strings.TrimSuffix(str, " ") + ret = strings.ToLower(str) + return ret +} diff --git a/values.go b/values.go new file mode 100644 index 0000000..a04a25a --- /dev/null +++ b/values.go @@ -0,0 +1,53 @@ +package saggytrousers + +import ( + xl "anyxcelize" + "fmt" +) + +// Provides functions for getting the header as []string and the body (except +// the header) as [][]any, which can be passed into other functions from there. + +func GetHeader(file *xl.File, sheet string) []string { + rows, err := file.Rows(sheet) + if err != nil { + str := fmt.Sprintf("st.GetHeader: call to Rows(sheet: %s) failed with error %v", sheet, err) + panic(str) + } + + rows.Next() + header, err := rows.Columns() + if err != nil { + str := fmt.Sprintf("st.GetHeader: Could not get a header.") + panic(str) + } + + return header +} + +// Get all rows, except the header. +func GetRows(file *xl.File, sheet string) [][]any { + rows, err := file.GetRowsGeneric(sheet) + if err != nil { + str := fmt.Sprintf("saggytrousers.GetRows: call to GetRowsGeneric(%s) failed with error %v", sheet, err) + panic(str) + } + + return rows[1:] +} + +func GetColumns(file *xl.File, sheet string) [][]any { + cols, err := file.GetColsGeneric(sheet) + if err != nil { + str := fmt.Sprintf("saggytrousers.GetColumns: call to GetRowsGeneric(%s) failed with error %v", sheet, err) + panic(str) + } + + var ret [][]any + for _, sa := range cols { + exceptFirst := sa[1:] + ret = append(ret, exceptFirst) + } + + return ret +} |