Skip to content

Instantly share code, notes, and snippets.

@corburn
Last active August 29, 2015 14:01
Show Gist options
  • Save corburn/f8ec8ff37a3d477ac992 to your computer and use it in GitHub Desktop.
Save corburn/f8ec8ff37a3d477ac992 to your computer and use it in GitHub Desktop.
Script to add a command to the Windows right-click context menu for text files
// Copyright Jason Travis
package main
import (
"encoding/csv"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"unicode"
)
const (
header = "ITEM ID,Item Description,Item Class,Inactive,Subject to Commission," +
"Description for Sales,Description for Purchases,Sales Price 1,Sales Price 1 Calculation," +
"Sales Price 1 Rounding,Sales Price 1 Rounding #,Sales Price 2,Sales Price 2 Calculation," +
"Sales Price 2 Rounding,Sales Price 2 Rounding #,Sales Price 3,Sales Price 3 Calculation," +
"Sales Price 3 Rounding,Sales Price 3 Rounding #,Sales Price 4,Sales Price 4 Calculation," +
"Sales Price 4 Rounding,Sales Price 4 Rounding #,Sales Price 5,Sales Price 5 Calculation," +
"Sales Price 5 Rounding,Sales Price 5 Rounding #,Sales Price 6,Sales Price 6 Calculation," +
"Sales Price 6 Rounding,Sales Price 6 Rounding #,Sales Price 7,Sales Price 7 Calculation," +
"Sales Price 7 Rounding,Sales Price 7 Rounding #,Sales Price 8,Sales Price 8 Calculation," +
"Sales Price 8 Rounding,Sales Price 8 Rounding #,Sales Price 9,Sales Price 9 Calculation," +
"Sales Price 9 Rounding,Sales Price 9 Rounding #,Sales Price 10,Sales Price 10 Calculation," +
"Sales Price 10 Rounding,Sales Price 10 Rounding #,Quanity Disc.,Item Tax Type,Last Unit Cost," +
"Costing Method,G/L Sales Account,G/L Inventory Account,G/L COGS/Salary Acct,UPC / SKU," +
"Item Type,Location,Stocking U/M,Use Multi-Packs,Purchasing U/M,Purchasing U/M Description," +
"Purch U/M No. of Stocking Units,Purchasing Weight,Purchasing UPC / SCC,Use Purchasing U/M as Default," +
"Sales U/M,Sales U/M Description,Sales U/M No. of Stocking Units,Sales Weight,Sales UPC / SCC," +
"Use Sales U/M as Default,Weight,Minimum Stock,Reorder Quantity,Vender ID,Buyer ID,Alternate Vendor," +
"Substitution,Special Note,Special Order,Master Stock Item ID,Primary Attrib. ID 1," +
"Primary Attrib. Desc. 1,Primary Attrib. ID 2,Primary Attrib. Desc. 2,Primary Attrib. ID 3," +
"Primary Attrib. Desc. 3,Primary Attrib. ID 4,Primary Attrib. Desc. 4,Primary Attrib. ID 5," +
"Primary Attrib. Desc. 5,Primary Attrib. ID 6,Primary Attrib. Desc. 6,Primary Attrib. ID 7," +
"Primary Attrib. Desc. 7,Primary Attrib. ID 8,Primary Attrib. Desc. 8,Primary Attrib. ID 9," +
"Primary Attrib. Desc. 9,Primary Attrib. ID 10,Primary Attrib. Desc. 10,Primary Attrib. ID 11," +
"Primary Attrib. Desc. 11,Primary Attrib. ID 12,Primary Attrib. Desc. 12,Primary Attrib. ID 13," +
"Primary Attrib. Desc. 13,Primary Attrib. ID 14,Primary Attrib. Desc. 14,Primary Attrib. ID 15," +
"Primary Attrib. Desc. 15,Primary Attrib. ID 16,Primary Attrib. Desc. 16,Primary Attrib. ID 17," +
"Primary Attrib. Desc. 17,Primary Attrib. ID 18,Primary Attrib. Desc. 18,Primary Attrib. ID 19," +
"Primary Attrib. Desc. 19,Primary Attrib. ID 20,Primary Attrib. Desc. 20,Secondary Attrib. ID 1," +
"Secondary Attrib. Desc. 1,Secondary Attrib. ID 2,Secondary Attrib. Desc. 2,Secondary Attrib. ID 3," +
"Secondary Attrib. Desc. 3,Secondary Attrib. ID 4,Secondary Attrib. Desc. 4,Secondary Attrib. ID 5," +
"Secondary Attrib. Desc. 5,Secondary Attrib. ID 6,Secondary Attrib. Desc. 6,Secondary Attrib. ID 7," +
"Secondary Attrib. Desc. 7,Secondary Attrib. ID 8,Secondary Attrib. Desc. 8,Secondary Attrib. ID 9," +
"Secondary Attrib. Desc. 9,Secondary Attrib. ID 10,Secondary Attrib. Desc. 10,Secondary Attrib. ID 11," +
"Secondary Attrib. Desc. 11,Secondary Attrib. ID 12,Secondary Attrib. Desc. 12,Secondary Attrib. ID 13," +
"Secondary Attrib. Desc. 13,Secondary Attrib. ID 14,Secondary Attrib. Desc. 14,Secondary Attrib. ID 15," +
"Secondary Attrib. Desc. 15,Secondary Attrib. ID 16,Secondary Attrib. Desc. 16,Secondary Attrib. ID 17," +
"Secondary Attrib. Desc. 17,Secondary Attrib. ID 18,Secondary Attrib. Desc. 18,Secondary Attrib. ID 19," +
"Secondary Attrib. Desc. 19,Secondary Attrib. ID 20,Secondary Attrib. Desc. 20,Item Note,Print Components," +
"Number of Components,Primary Attrib. Name,Substock Primary Attrib. ID,Substock Primary Attrib. Desc.," +
"Secondary Attrib. Name,Substock Second Attrib. ID,Substock Second Attrib. Desc.,Warranty Period," +
"Revision Number,Effective Date,Component Number,Component ID,Quantity Needed,Part Number"
headerLength = 177
)
var pwd = os.Getenv("USERPROFILE")
var vendorFilename = filepath.FromSlash(pwd + "/m2p/vendor-list.csv")
var logFilename = filepath.FromSlash(pwd + "/m2p/m2p.log")
func main() {
var inFile, outFile, vendorFile, logFile *os.File
var err error
if len(os.Args) < 2 {
log.Fatal("Usage: m2p AutoPartPrices.txt")
}
log.Println("Opening " + logFilename)
logFile, err = os.OpenFile(logFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatal(err)
}
defer logFile.Close()
log.SetOutput(logFile)
log.SetFlags(log.Ldate | log.Lshortfile)
//log.SetPrefix(os.Args[1] + " ")
log.Println("Initializing")
log.Printf("Command line arguments: %v", os.Args)
log.Println("Loading " + os.Args[1])
inFile, err = os.Open(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer inFile.Close()
inventory := NewInventory(inFile)
log.Println("Loading " + vendorFilename)
vendorFile, err = os.Open(vendorFilename)
if err != nil {
log.Fatal(err)
}
defer vendorFile.Close()
vendor, err := NewVendor(vendorFile)
if err != nil {
log.Fatal(err)
}
// Filter similar entries
if err := inventory.filter(vendor); err != nil {
log.Fatal(err)
}
// Convert to a format Peachtree can import
if err := inventory.format(); err != nil {
log.Fatal(err)
}
// Replace the inFile extension with .csv to get the outFileName
outFilename := inFile.Name()[0:len(inFile.Name())-len(filepath.Ext(inFile.Name()))] + ".csv"
log.Println("Saving " + outFilename)
outFile, err = os.Create(outFilename)
if err != nil {
log.Fatal(err)
return
}
defer outFile.Close()
inventory.save(outFile)
log.Println("Done")
}
// Inventory TODO
type Inventory [][]string
// NewInventory TODO
func NewInventory(r io.Reader) Inventory {
// Read inventory file
inventory, err := parseCSV(r)
if err != nil {
log.Fatal(err)
}
return Inventory(inventory)
}
func (inventory *Inventory) filter(vendor Vendor) error {
var n = len(*inventory)
if n < 2 {
return nil
}
var filtered = make(Inventory, 0, n)
// Potential unit tests
// empty inventory
// odd inventory
// 1 duplicate
// 2 duplicates
// duplicates in series
// duplicate-unique-duplicate
log.Printf("Total pre-filter: %d\n", len(*inventory))
var duplicates = 0
var ptr = 0
for i := 1; i < n; i++ {
if (*inventory)[i][1] == (*inventory)[ptr][1] {
var p, c int
var ok bool
var err error
duplicates++
log.Println("duplicate item: " + (*inventory)[i][1] + " vendor " + (*inventory)[i][16] + " and " + (*inventory)[ptr][16])
if p, ok = vendor[(*inventory)[ptr][16]]; !ok {
log.Println("APPEND vendor " + (*inventory)[ptr][16] + " to " + vendorFilename)
p = len(vendor) - 1
err = vendor.append((*inventory)[ptr][16])
if err != nil {
return err
}
}
if c, ok = vendor[(*inventory)[i][16]]; !ok {
log.Println("APPEND vendor " + (*inventory)[i][16] + " to " + vendorFilename)
c = len(vendor) - 1
err = vendor.append((*inventory)[i][16])
if err != nil {
return err
}
}
if p > c {
log.Printf("Skipping %s %s vendor priority %d\n", (*inventory)[ptr][1], (*inventory)[ptr][16], p)
ptr = i
} else {
log.Printf("Skipping %s %s vendor priority %d\n", (*inventory)[ptr][1], (*inventory)[ptr][16], p)
}
} else {
filtered = append(filtered, (*inventory)[ptr])
ptr = i
}
}
filtered = append(filtered, (*inventory)[ptr])
log.Printf("Duplicates: %d\n", duplicates)
log.Printf("Expected total: %d\n", len(*inventory)-duplicates)
log.Printf("Total post-filter: %d\n", len(filtered))
*inventory = filtered
return nil
}
// Vendor TODO
type Vendor map[string]int
// NewVendor TODO
func NewVendor(r io.Reader) (Vendor, error) {
vendors, err := parseCSV(r)
if err != nil {
return nil, err
}
if len(vendors) < 1 || len(vendors[0]) < 1 {
return nil, errors.New(vendorFilename + " is empty")
}
dict := make(Vendor, len(vendors[0]))
for i, v := range vendors[0] {
dict[v] = i
}
return dict, nil
}
func (v *Vendor) append(vendor string) error {
vendorFile, err := os.OpenFile(vendorFilename, os.O_RDWR, 0644)
if err != nil {
return err
}
defer vendorFile.Close()
vendors, err := parseCSV(vendorFile)
if err != nil {
return err
}
vendors[0] = append(vendors[0], vendor)
// Rewind to the beginning of the file
vendorFile.Seek(0, os.SEEK_SET)
writer := csv.NewWriter(vendorFile)
if err := writer.WriteAll(vendors); err != nil {
return err
}
(*v)[vendor] = len(*v) - 1
return nil
}
func parseCSV(r io.Reader) ([][]string, error) {
reader := csv.NewReader(r)
reader.LazyQuotes = true
//reader.FieldsPerRecord = 20
//reader.TrimLeadingSpace = true
records, err := reader.ReadAll()
// Trim whitespace and quotes from all fields
for i := range records {
for j := range records[i] {
records[i][j] = strings.TrimFunc(records[i][j], func(r rune) bool {
return unicode.IsSpace(r) || r == '"'
})
}
}
return records, err
}
func (inventory *Inventory) format() error {
if len(*inventory) < 1 {
return errors.New("format error: empty input")
}
if len((*inventory)[0]) != 20 {
return fmt.Errorf("format error: expected input with 20 columns, found %d\n", len((*inventory)[0]))
}
// TODO validate input
var output = make(Inventory, len(*inventory))
for i := range *inventory {
output[i] = make([]string, headerLength)
// Fill in output columns with values from the input file.
// input[0] UNUSED. input[8] contains the complete Item Type
// input[4 and 5] Discount and Special Market UNUSED.
// input[7,9,12-15] UNKNOWN.
// input[17] DUPLICATE of input[1], but without the quotes
// In descending order, right-to-left, reduce Sales Price by 3%
salesPrice9 := 39
salesPrice1 := 6
stepSize := 4
jobber, err := strconv.ParseFloat((*inventory)[i][3], 64)
if err != nil {
return err
}
for salesPriceN := salesPrice9; salesPriceN > salesPrice1; salesPriceN -= stepSize {
// Convert jobber to a string with no exponent, infinite precision, assume the original was a float64
output[i][salesPriceN] = strconv.FormatFloat(jobber, 'f', -1, 64)
jobber *= .97 // .97 = 97% = reduce by 3%
}
output[i][0] = (*inventory)[i][1] // Item ID. (*inventory)[i]Tokens[1 or 17]
output[i][43] = (*inventory)[i][2] // List
output[i][49] = (*inventory)[i][6] // Last Unit Cost
output[i][55] = strings.Replace((*inventory)[i][8], "-", "", -1) // Item Type
output[i][56] = (*inventory)[i][10] // Location
output[i][1] = (*inventory)[i][11] // Description
output[i][5] = (*inventory)[i][11] // Description
output[i][6] = (*inventory)[i][11] // Description
output[i][74] = (*inventory)[i][16] // Vender ID
// Fill in output columns with default values not provided by the (*inventory) file.
// The typos are deliberate to reflect the Header element values.
output[i][2] = "1" // Item Class
output[i][3] = "FALSE" // Inactive
output[i][4] = "TRUE" // Subject to Commission
//output[i][47] = "FALSE" // Quantity Disc.
output[i][48] = "2" // Item Tax Type
output[i][50] = "1" // Costing Method
output[i][51] = "4000" // G/L Sales Account
output[i][52] = "1200" // G/L Inventory Account
output[i][53] = "4500" // G/L COGS/Salary Acct
output[i][58] = "FALSE" // Use Multi-Packs
output[i][61] = "0" // Purch U/M No. of Stocking Units
output[i][62] = "0" // Purchasing Weight
output[i][64] = "FALSE" // Use Purchasing U/M as Default
output[i][67] = "0" // Sales U/M No. of Stocking Units
output[i][68] = "0" // Sales Weight
output[i][69] = "0" // Sales UPC / SCC
output[i][70] = "FALSE" // Use Sales U/M as Default
output[i][71] = "0" // Weight
output[i][72] = "0" // Minimum Stock
output[i][73] = "0" // Reorder Quantity
output[i][75] = "1" // Buyer ID
// The remaining output columns are unused.
}
*inventory = output
return nil
}
func (inventory *Inventory) save(w io.Writer) error {
// Write header
if _, err := w.Write([]byte(header + "\n")); err != nil {
return err
}
// Write formatted inventory
writer := csv.NewWriter(w)
if err := writer.WriteAll([][]string(*inventory)); err != nil {
return err
}
return nil
}
@echo "The root directory:"
@echo %USERPROFILE%
@echo "Check 1/2: Does the txtfile key exist?"
@REG QUERY HKEY_CLASSES_ROOT\txtfile
PAUSE
@echo "Check 2/2: Does the txtfile\shell key exist?"
@REG QUERY HKEY_CLASSES_ROOT\txtfile\shell
PAUSE
@ECHO "The next instructions will add a m2p command key"
PAUSE
@REG ADD HKEY_CLASSES_ROOT\txtfile\shell\m2p
@REG ADD HKEY_CLASSES_ROOT\txtfile\shell\m2p\command /ve /t REG_EXPAND_SZ /d "%USERPROFILE%\m2p\m2p.exe \"%1\""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment