-
Notifications
You must be signed in to change notification settings - Fork 6
/
validator.go
131 lines (120 loc) · 3.3 KB
/
validator.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
package runner
import (
"fmt"
base "github.com/omegaup/go-base/v2"
"github.com/omegaup/quark/common"
"io"
"math"
"math/big"
"strconv"
"strings"
)
// CalculateScore calculates the score of a contestantOutput by comparing it
// with the expectedOutput under the specified validator settings.
func CalculateScore(
settings *common.ValidatorSettings,
expectedOutput, contestantOutput io.Reader,
) (*big.Rat, *TokenMismatch, error) {
scanFunc := IsNonWhitespace
if settings.Name == "token-numeric" {
scanFunc = IsNumeric
}
contestantTokenizer := NewTokenizer(contestantOutput, scanFunc)
if settings.Name == "literal" || settings.Name == "custom" {
if !contestantTokenizer.Scan() {
return &big.Rat{}, nil, io.ErrUnexpectedEOF
}
value, err := base.ParseRational(contestantTokenizer.Token().Text)
if err != nil {
return &big.Rat{}, nil, err
}
return ratClamp(value, &big.Rat{}, big.NewRat(1, 1)), nil, nil
}
expectedTokenizer := NewTokenizer(expectedOutput, scanFunc)
var mismatch *TokenMismatch
for mismatch == nil {
expectedNext := expectedTokenizer.Scan()
contestantNext := contestantTokenizer.Scan()
if expectedNext != contestantNext {
mismatch = &TokenMismatch{}
if expectedNext {
mismatch.Expected = expectedTokenizer.Token()
}
if contestantNext {
mismatch.Contestant = contestantTokenizer.Token()
}
}
if !expectedNext || !contestantNext {
if !expectedNext && expectedTokenizer.Err() != nil {
return &big.Rat{}, mismatch, expectedTokenizer.Err()
}
if !contestantNext && contestantTokenizer.Err() != nil {
return &big.Rat{}, mismatch, contestantTokenizer.Err()
}
break
}
expectedToken := expectedTokenizer.Token()
contestantToken := contestantTokenizer.Token()
correct := true
switch settings.Name {
case "token":
correct = tokenEquals(expectedToken.Text, contestantToken.Text)
case "token-caseless":
correct = tokenCaselessEquals(expectedToken.Text, contestantToken.Text)
case "token-numeric":
tolerance := common.DefaultValidatorTolerance
if settings.Tolerance != nil {
tolerance = *settings.Tolerance
}
correct = tokenNumericEquals(
expectedToken.Text,
contestantToken.Text,
tolerance,
)
default:
return &big.Rat{}, nil, fmt.Errorf("Unknown validator: %q", settings.Name)
}
if !correct {
mismatch = &TokenMismatch{
Contestant: contestantToken,
Expected: expectedToken,
}
}
}
if mismatch != nil {
return &big.Rat{}, mismatch, nil
}
return big.NewRat(1, 1), nil, nil
}
func tokenEquals(a, b string) bool {
return a == b
}
func tokenCaselessEquals(a, b string) bool {
return strings.EqualFold(a, b)
}
func tokenNumericEquals(a, b string, tolerance float64) bool {
af, erra := strconv.ParseFloat(a, 64)
bf, errb := strconv.ParseFloat(b, 64)
if erra != nil || errb != nil {
return erra != nil && errb != nil
}
const SmallestNormal = 2.2250738585072014e-308 // 2**-1022
diff := math.Abs(bf - af)
if af == bf {
return true
} else if diff <= 1.5*tolerance {
return true
} else if af == 0 || bf == 0 || diff < SmallestNormal {
return diff <= tolerance*SmallestNormal
}
return diff/math.Max(math.Abs(af), math.Abs(bf)) <= tolerance
}
func ratClamp(val, min, max *big.Rat) *big.Rat {
if val.Cmp(min) <= 0 {
return min
}
if val.Cmp(max) >= 0 {
return max
}
return val
}