forked from google/simhospital
/
value_generator.go
366 lines (331 loc) · 12.1 KB
/
value_generator.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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package orderprofile
import (
"fmt"
"math/rand"
"regexp"
"github.com/pkg/errors"
"github.com/google/simhospital/pkg/constants"
)
const valueFormat = "%.2f"
var (
fromToRangeRegExp = []*regexp.Regexp{
// simple ranges,ie:
// from-to
// from - to
// [ from - to ]
// [from-to]
regexp.MustCompile("^\\[? ?(-?[0-9|/.]+) ?- ?(-?[0-9|/.]+) ?\\]?$"),
// duplicated value ranges, ie:
// from-to^from^to
regexp.MustCompile("^(-?[0-9|/.]+)-(-?[0-9|/.]+)(\\^-?[0-9|/.]+)+$"),
}
lessRangeRegExp = []*regexp.Regexp{
// less than ranges, ie:
// <to^^<to
// <to^<to
// <=to^^<=to
// <=to^<=to
regexp.MustCompile("^<=?(-?[0-9|/.]+)\\^+<=?-?[0-9|/.]+$"),
// less than ranges with brackets, ie:
// [ < to ]
// [ <= to ]
// [<to]
// [<=to]
regexp.MustCompile("^\\[? ?<=? ?(-?[0-9|/.]+) ?\\]?$"),
}
greaterRangeRegExp = []*regexp.Regexp{
// greater than ranges, ie:
// >from^^>from
// >from^>from
// >=from^^>=from
// >=from^>=from
regexp.MustCompile("^>=?(-?[0-9|/.]+)\\^+>=?-?[0-9|/.]+$"),
// greater than ranges with brackets, ie:
// [ > from ]
// [ >= from ]
// [>from]
// [>=from]
regexp.MustCompile("^\\[? ?>=? ?(-?[0-9|/.]+) ?\\]?$"),
}
)
// ValueGenerator generates the value within the range (exclusive) given the normal value range.
//
// The ranges specified for the order profiles may sometimes be:
// inclusive for one border, eg.: >=5.5,
// exclusive, eg.: >0.25
// unknown inclusivity, eg.: 0.5 - 1.6 - probably exclusive, 70-120^70^120 - probably inclusive
// It is though safer (and easier) to always treat the range as exclusive.
type ValueGenerator struct {
from validFloat
to validFloat
}
type validFloat struct {
value float64
valid bool
}
func newValidFloat(f float64) validFloat {
return validFloat{value: f, valid: true}
}
func newInvalidFloat() validFloat {
return validFloat{valid: false}
}
// IsHigh returns whether the value v is above the range represented by ValueGenerator.
func (g *ValueGenerator) IsHigh(v float64) bool {
return g.to.valid && v > g.to.value
}
// IsLow returns whether the value v is below the range represented by ValueGenerator.
func (g *ValueGenerator) IsLow(v float64) bool {
return g.from.valid && v < g.from.value
}
// IsNormal returns whether the value v is within range represented by ValueGenerator.
func (g *ValueGenerator) IsNormal(v float64) bool {
if g.from.valid && g.to.valid {
return v >= g.from.value && v <= g.to.value
}
return (g.from.valid && v >= g.from.value) || (g.to.valid && v <= g.to.value)
}
// Random returns the random value based on the randomType, which is either within normal ranges,
// or outside the normal ranges (ie: higher or lower).
// Returns error if the random value cannot be generated.
func (g *ValueGenerator) Random(randomType string) (string, error) {
switch randomType {
case constants.AbnormalLow:
return g.AbnormalLow()
case constants.AbnormalHigh:
return g.AbnormalHigh()
case constants.NormalValue:
return g.Normal()
default:
log.WithField("random_type", randomType).Error("Unknown random type")
return "", errors.New("unknown random type")
}
}
// Normal returns random number formatted as string within the normal range,
// ie between (g.from, g.to) exclusive.
//
// If ValueGenerator represents a right open range, ie: >g.from (g.to is invalid):
// - if "from" is positive, the normal value is generated from (g.from, 10 x g.from).
// - if "from" is negative, the normal value is generated from (g.from, 0) to only allow negative numbers.
// - if "from" is 0, the normal value is generated from (g.from, 10); this is an arbitrary choice,
// as we don't really know what order of magnitude the values should be in.
//
// If ValueGenerator represents a left open range, ie: <g.to (g.from is invalid):
// - if "to" is positive, the normal value is generated from (0, g.to) to only allow positive values.
// - if "to" is negative, the normal value is generated from (10 x g.to, g.to).
// - if "to" is 0, the normal value is generated from (-10, g.to); this is an arbitrary choice,
// as we don't really know what order of magnitude the values should be in.
//
// If g == nil, returns 0.
// Returns error if both: start and end of the range are open.
func (g *ValueGenerator) Normal() (string, error) {
if g == nil {
return fmt.Sprintf(valueFormat, 0.0), nil
}
if !g.from.valid && !g.to.valid {
log.WithField("value_generator", g).Error("Cannot generate normal value if both: start and end of the range are open")
return "", errors.New("cannot generate normal value if both: start and end of the range are open")
}
var from, to float64
if g.from.valid {
from = g.from.value
} else if g.to.value == 0 {
// if end of the range is equal to 0, start of the range is set to -10; this is an arbitrary choice,
// as we don't really know what order of magnitude the values should be in
from = -10
} else if g.to.value > 0 {
// if end of range is positive, set "from" to 0 to only allow positive numbers
from = 0
} else {
// if end of range is negative, set "from" to 10 x start of the range,
// to avoid absurd small numbers being generated
from = g.to.value * 10
}
if g.to.valid {
to = g.to.value
} else if from == 0 {
// if start of the range is equal to 0, end of the range is set to 10; this is an arbitrary choice,
// as we don't really know what order of magnitude the values should be in
to = 10
} else if from > 0 {
// if the start of the range is positive, set end of the range to 10 x start of the range,
// to avoid absurd huge numbers being generated
to = from * 10
} else {
// if end of the range is negative, set end of the range to 0 to only allow negative numbers
to = 0
}
return randomFromRange(from, to)
}
// AbnormalLow returns a random number formatted as string, which is lower than the normal range.
// If the start of normal range is positive, it will return value between (0, g.from) exclusive.
// If the start of normal range is negative, it will return value between (10 x g.from, g.from) exclusive.
// Returns an error in any of the following situations:
// - the receiver is nil
// - the start of the normal range is open
// - the start of the normal range is 0 -> the assumption is that if start of the normal range is positive,
// the negative numbers are invalid, thus it is impossible to generate the abnormal low value if range starts at 0
func (g *ValueGenerator) AbnormalLow() (string, error) {
if g == nil {
return "", errors.New("cannot generate abnormal low value for nil ValueGenerator")
}
if !g.from.valid || g.from.value == 0 {
log.WithField("value_generator", g).Error("Cannot generate abnormal low value for open or zero start range")
return "", errors.New("cannot generate abnormal low value for open or zero start range")
}
var from, to float64
if g.from.value > 0 {
from, to = 0, g.from.value
} else {
from, to = 10*g.from.value, g.from.value
}
return randomFromRange(from, to)
}
// AbnormalHigh returns a random number formatted as string, which is higher than the normal range.
// If the end of normal range is positive, it will return value between (g.to, 10 x g.to) exclusive.
// If the end of normal range is negative, it will return value between (g.to, 0) exclusive.
// Returns an error in any of the following situations:
// - the receiver is nil
// - the end of the normal range is open
// - the end of the normal range is 0 -> the assumption is that if the end of the normal range is negative,
// the positive numbers are invalid, thus it is impossible to generate the abnormal high value if range ends at 0
func (g *ValueGenerator) AbnormalHigh() (string, error) {
if g == nil {
return "", errors.New("cannot generate abnormal high value for nil ValueGenerator")
}
if !g.to.valid || g.to.value == 0 {
log.WithField("value_generator", g).Error("Cannot generate abnormal high value for open or zero end range")
return "", errors.New("cannot generate abnormal high value for open or zero end range")
}
var from, to float64
if g.to.value > 0 {
from, to = g.to.value, 10*g.to.value
} else {
from, to = g.to.value, 0
}
return randomFromRange(from, to)
}
func randomFromRange(from float64, to float64) (string, error) {
for i := 0; i < 100; i++ {
f := rand.Float64()*(to-from) + from
// The rand.Float64() returns value between [0.0, 1.0), ie the start of the range is inclusive, while
// the number generated by the ValueGenerator needs to be exclusive.
// Furthermore, the value is formatted as a string, when it loses some precisions, ie. it is formatted
// to 2 decimal places only.
// We need to ensure, that the generated value is still within the range (exclusive for both: start and end
// of the range) after the value is formatted.
s := fmt.Sprintf(valueFormat, f)
if afterFormatting, _ := floatFromString(s); afterFormatting > from && afterFormatting < to {
return s, nil
}
}
log.WithField("from", from).WithField("to", to).Error("Failed to generate the value from range after 100 attempts")
return "", errors.New("failed to generate the value from range after 100 attempts")
}
// ValueGeneratorFromRange returns ValueGenerator created from string
// containing the normal value range.
// String may be in one of the following formats:
//
// from-to
// from - to
// [ from - to ]
// [from-to]
// from-to^from^to
// where both: "from" and "to" are either positive or negative floating point numbers
//
// or:
//
// <to^^<to
// <to^<to
// <=to^^<=to
// <=to^<=to
// [ < to ]
// [ <= to ]
// [<to]
// [<=to]
// where "to" is either positive or negative floating point number; "from" is set to invalid float (open start range)
//
// or:
//
// >from^^>from
// >from^>from
// >=from^^>=from
// >=from^>=from
// [ > from ]
// [ >= from ]
// [>from]
// [>=from]
// where "from" is either positive or negative floating point number; "to" is set to invalid float (open end of range)
//
// Returns error if the string cannot be parsed.
func ValueGeneratorFromRange(s string) (*ValueGenerator, error) {
var matchGroups []string
for _, r := range fromToRangeRegExp {
if g := r.FindStringSubmatch(s); len(g) >= 3 {
matchGroups = g
break
}
}
if len(matchGroups) >= 3 {
from, err := floatFromString(matchGroups[1])
if err != nil {
return nil, fmt.Errorf("failed to parse from value: %s", matchGroups[1])
}
to, err := floatFromString(matchGroups[2])
if err != nil {
return nil, fmt.Errorf("failed to parse to value: %s", matchGroups[2])
}
if from >= to {
return nil, fmt.Errorf("Cannot create ValueGenerator: start of the range [%f] "+
"is greater than end of the range [%f]", from, to)
}
return &ValueGenerator{
from: newValidFloat(from),
to: newValidFloat(to),
}, nil
}
return valueGeneratorFromLessGreaterRange(s)
}
func valueGeneratorFromLessGreaterRange(s string) (*ValueGenerator, error) {
for _, r := range lessRangeRegExp {
matchGroups := r.FindStringSubmatch(s)
if len(matchGroups) != 2 {
continue
}
to, err := floatFromString(matchGroups[1])
if err != nil {
return nil, fmt.Errorf("failed to parse to value of less range: %s", matchGroups[1])
}
return &ValueGenerator{
from: newInvalidFloat(),
to: newValidFloat(to),
}, nil
}
for _, r := range greaterRangeRegExp {
matchGroups := r.FindStringSubmatch(s)
if len(matchGroups) != 2 {
continue
}
from, err := floatFromString(matchGroups[1])
if err != nil {
return nil, fmt.Errorf("failed to parse from value of greater range: %s", matchGroups[1])
}
return &ValueGenerator{
from: newValidFloat(from),
to: newInvalidFloat(),
}, nil
}
return nil, fmt.Errorf("failed to parse the range: %s", s)
}