-
Notifications
You must be signed in to change notification settings - Fork 0
/
cex.go
175 lines (163 loc) · 4.35 KB
/
cex.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// Package cexfind searches for devices for sale at Cex/Webuy via the
// unofficial "webuy.io" query endpoint which responds in a json format.
//
// Queries are required to be made in the UK as the endpoint is
// protected by region-sensitive CDN.
//
// Multiple concurrent queries are supported, with an optional "strict"
// flag to constrain results to the query terms. The results are a union
// of the results of each query, ordered by model name and then the
// price of each item.
//
// Example usage:
//
// results, err := cex.Search(queries, strict)
// if err != nil {
// log.Fatal(err)
// }
//
// latestModel := ""
// for _, box := range results {
// if box.Model != latestModel {
// fmt.Printf("\n%s\n", box.Model)
// latestModel = box.Model
// }
// fmt.Printf(
// " £%3d %s\n %s\n",
// box.Price,
// box.Name,
// box.IDUrl(),
// )
// }
package cexfind
import (
"cmp"
"errors"
"fmt"
"slices"
"strings"
"github.com/shopspring/decimal"
)
// Box is a very simplified representation of a Cex/Webuy json entry,
// where each entry represents a "Box" or computer or other item for
// sale.
type Box struct {
Model string
Name string
ID string
Price decimal.Decimal
PriceCash decimal.Decimal
PriceExchange decimal.Decimal
Stores []string
}
// inQuery checks to see if each of the words in at least one of the
// supplied queries are in the Name of a Box. inQuery is used for
// determining if a particular Box should be returned from a "strict"
// search.
func (b *Box) inQuery(queries []string) bool {
for _, q := range queries {
matches := 0
name := strings.ToLower(b.Name)
words := strings.Split(strings.ToLower(q), " ")
for _, w := range words {
if strings.Contains(name, w) {
matches++
}
}
if matches == len(words) {
return true
}
}
return false
}
// IDUrl returns the full url path to the Cex/Webuy webpage showing the
// Box (the item of equipment) in question.
func (b Box) IDUrl() string {
return urlDetail + b.ID
}
// reverseID is useful for sorting because the grade of the box is the
// right-most character. The grade cannot be conveniently extracted
// otherwise. For the same price, a higher grade (eg B) is prefereable
// over a lower grade (eg C).
func (b *Box) reverseID() string {
r := []rune(b.ID)
for i, j := 0, len(b.ID)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
// StoresString returns the stores as a comma delimited string,
// truncating the list if necessary
func (b *Box) StoresString() string {
var storeMaxCnt int = 5
if len(b.Stores) == 0 {
return ""
}
if len(b.Stores) > storeMaxCnt {
return fmt.Sprintf(
"%s...(%d more)",
strings.Join(b.Stores[:storeMaxCnt], ", "),
len(b.Stores)-storeMaxCnt,
)
}
return strings.Join(b.Stores, ", ")
}
// boxes is a slice of Box
type boxes []Box
// sort sorts boxes by box.Model then Price ascending then ID.
func (b *boxes) sort() {
slices.SortFunc(*b, func(i, j Box) int {
var c int
c = cmp.Compare(i.Model, j.Model)
if c != 0 {
return c
}
c = i.Price.Compare(j.Price)
if c != 0 {
return c
}
// the most right char is the box condition (A, B or C)
return cmp.Compare(i.reverseID(), j.reverseID())
})
}
// Search searches the Cex json endpoint at URL for the provided
// queries, returning a slice of Box or error.
//
// The strict flag ensures that the results contain terms from the
// search queries as the non-strict results include additional
// suggestions from the Cex/Webuy system.
//
// Multiple queries are run concurrently and their results sorted by
// model, then by price ascending. Duplicate results are removed at
// aggregation.
func Search(queries []string, strict bool) ([]Box, error) {
var allBoxes boxes
var idMap = make(map[string]struct{})
var err error
results := makeQueries(queries, strict)
for br := range results {
if br.err != nil {
if err != nil {
err = fmt.Errorf("\"%s\": %w\n%w", br.query, br.err, err)
} else {
err = fmt.Errorf("\"%s\": %w", br.query, br.err)
}
continue
}
if _, ok := idMap[br.box.ID]; ok { // don't add duplicates
continue
}
allBoxes = append(allBoxes, br.box)
idMap[br.box.ID] = struct{}{}
}
allBoxes.sort()
if len(allBoxes) == 0 {
if err != nil {
err = fmt.Errorf("%w", err)
} else {
err = errors.New("no results")
}
return allBoxes, err
}
return allBoxes, err
}