spread

https://git.tonybtw.com/spread.git git://git.tonybtw.com/spread.git
3,224 bytes raw
1
package spread
2
3
import (
4
	"strings"
5
	"unicode"
6
)
7
8
// catalog holds the parts we can source at a fixed unit price, keyed by
9
// normalized part number. In the demo these are hardcoded; in production
10
// this map would be backed by live supplier quotes.
11
//
12
// The five parts below match the canonical example on the landing page, so
13
// the shipped example-bom.csv reproduces the exact $883.50 savings figure.
14
var catalog = map[string]float64{
15
	"GRM188R71H104KA93D": 0.0142,
16
	"DIN 912":            0.0335,
17
	"UL1007":             0.9450,
18
	"STM32F103C8T6":      2.9200,
19
	"RC0805FR-0710KL":    0.0086,
20
}
21
22
// categoryDiscount maps a keyword found in a part's description to the
23
// fraction we can typically shave off market price for that category.
24
// Ordering matters: the first keyword that matches wins.
25
var categoryDiscount = []struct {
26
	keyword  string
27
	discount float64
28
}{
29
	{"capacitor", 0.21},
30
	{"resistor", 0.28},
31
	{"inductor", 0.22},
32
	{"diode", 0.24},
33
	{"transistor", 0.24},
34
	{"mcu", 0.24},
35
	{"microcontroller", 0.24},
36
	{"ic", 0.23},
37
	{"regulator", 0.23},
38
	{"connector", 0.19},
39
	{"header", 0.19},
40
	{"led", 0.26},
41
	{"crystal", 0.20},
42
	{"oscillator", 0.20},
43
	{"screw", 0.25},
44
	{"bolt", 0.25},
45
	{"nut", 0.25},
46
	{"washer", 0.27},
47
	{"fastener", 0.25},
48
	{"standoff", 0.24},
49
	{"wire", 0.21},
50
	{"cable", 0.21},
51
	{"pcb", 0.16},
52
	{"enclosure", 0.15},
53
	{"heatsink", 0.18},
54
}
55
56
// defaultDiscount applies when no category keyword matches.
57
const defaultDiscount = 0.15
58
59
// NormalizePN canonicalizes a part number for catalog lookups: uppercased,
60
// trimmed, internal whitespace collapsed. The app stores and looks up catalog
61
// keys with the same function so matching is consistent.
62
func NormalizePN(pn string) string {
63
	pn = strings.ToUpper(strings.TrimSpace(pn))
64
	return strings.Join(strings.Fields(pn), " ")
65
}
66
67
// Catalog is an exact-match supplier price source. Parts it doesn't know fall
68
// back to the category model inside the analyzer.
69
type Catalog interface {
70
	Lookup(partNumber string) (spreadPrice float64, ok bool)
71
}
72
73
// BuiltinCatalog is the demo's hardcoded catalog, used by the public endpoint.
74
type BuiltinCatalog struct{}
75
76
func (BuiltinCatalog) Lookup(partNumber string) (float64, bool) {
77
	p, ok := catalog[NormalizePN(partNumber)]
78
	return p, ok
79
}
80
81
// tokenize lowercases and splits text into whole words so short keywords
82
// like "ic" match only as words, not as substrings of "generic".
83
func tokenize(s string) map[string]bool {
84
	set := map[string]bool{}
85
	for _, f := range strings.FieldsFunc(strings.ToLower(s), func(r rune) bool {
86
		return !unicode.IsLetter(r) && !unicode.IsDigit(r)
87
	}) {
88
		set[f] = true
89
	}
90
	return set
91
}
92
93
// modelDiscount estimates a discount for a part we don't stock by name,
94
// based on its category plus a small volume bonus for large quantities.
95
func modelDiscount(li LineItem) float64 {
96
	words := tokenize(li.Description + " " + li.PartNumber)
97
	d := defaultDiscount
98
	for _, c := range categoryDiscount {
99
		if words[c.keyword] {
100
			d = c.discount
101
			break
102
		}
103
	}
104
	// Volume bonus: up to +4 points for high-quantity lines.
105
	switch {
106
	case li.Quantity >= 10000:
107
		d += 0.04
108
	case li.Quantity >= 1000:
109
		d += 0.02
110
	case li.Quantity >= 100:
111
		d += 0.01
112
	}
113
	if d > 0.45 {
114
		d = 0.45
115
	}
116
	return d
117
}