Last active
August 29, 2015 14:01
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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