@khanacademy/kas 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE.txt +21 -0
- package/README.md +94 -0
- package/dist/es/index.js +2 -0
- package/dist/es/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.flow +2 -0
- package/dist/index.js.map +1 -0
- package/experimenter.html +75 -0
- package/package.json +38 -0
- package/src/__genfiles__/parser.js +840 -0
- package/src/__genfiles__/unitparser.js +679 -0
- package/src/__tests__/checking-form_test.js +76 -0
- package/src/__tests__/comparing_test.js +322 -0
- package/src/__tests__/compilation_test.js +97 -0
- package/src/__tests__/evaluating_test.js +73 -0
- package/src/__tests__/index_test.js +364 -0
- package/src/__tests__/parsing_test.js +480 -0
- package/src/__tests__/rendering_test.js +272 -0
- package/src/__tests__/transforming_test.js +331 -0
- package/src/__tests__/units_test.js +188 -0
- package/src/compare.js +69 -0
- package/src/index.js +2 -0
- package/src/nodes.js +3504 -0
- package/src/parser-generator.js +212 -0
- package/src/unitvalue.jison +161 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import _ from "underscore";
|
|
2
|
+
|
|
3
|
+
import * as KAS from "../index.js";
|
|
4
|
+
|
|
5
|
+
expect.extend({
|
|
6
|
+
toHaveEqualUnits([x, y]: [$FlowFixMe, $FlowFixMe], msg: string) {
|
|
7
|
+
let actual = KAS.compare(x.simplify(), y.simplify()).equal;
|
|
8
|
+
|
|
9
|
+
if (this.isNot) {
|
|
10
|
+
return {
|
|
11
|
+
pass: actual,
|
|
12
|
+
message: () => msg,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
pass: actual,
|
|
18
|
+
message: () => msg,
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
toParseUnitsAsEqual([x, y]: [string, string], msg: string) {
|
|
22
|
+
if (this.isNot) {
|
|
23
|
+
expect([
|
|
24
|
+
KAS.unitParse(x).expr,
|
|
25
|
+
KAS.unitParse(y).expr,
|
|
26
|
+
]).not.toHaveEqualUnits(msg);
|
|
27
|
+
} else {
|
|
28
|
+
expect([
|
|
29
|
+
KAS.unitParse(x).expr,
|
|
30
|
+
KAS.unitParse(y).expr,
|
|
31
|
+
]).toHaveEqualUnits(msg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {pass: !this.isNot};
|
|
35
|
+
},
|
|
36
|
+
toHaveUnitVariable(
|
|
37
|
+
[original, newUnit]: [string, string],
|
|
38
|
+
expected: $FlowFixMe,
|
|
39
|
+
) {
|
|
40
|
+
const originalParsed = KAS.unitParse(original).expr;
|
|
41
|
+
const newUnitParsed = KAS.unitParse(newUnit).unit;
|
|
42
|
+
const x = new KAS.Var("x");
|
|
43
|
+
const equality = new KAS.Eq(
|
|
44
|
+
originalParsed,
|
|
45
|
+
"=",
|
|
46
|
+
new KAS.Mul(x, newUnitParsed),
|
|
47
|
+
);
|
|
48
|
+
const answer = equality.solveLinearEquationForVariable(x);
|
|
49
|
+
return Math.round(answer.eval()) == Math.round(expected.eval())
|
|
50
|
+
? {pass: true}
|
|
51
|
+
: {
|
|
52
|
+
pass: false,
|
|
53
|
+
message: () =>
|
|
54
|
+
`${original} = ["${expected.print()}"] ${newUnit}`,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
toHaveTheSameForm([x, y]: [string, string], msg: string) {
|
|
58
|
+
const equal = KAS.compare(
|
|
59
|
+
KAS.unitParse(x).unit,
|
|
60
|
+
KAS.unitParse(y).unit,
|
|
61
|
+
).equal;
|
|
62
|
+
|
|
63
|
+
return equal ? {pass: true} : {pass: false, message: () => msg};
|
|
64
|
+
},
|
|
65
|
+
toHaveMagnitude(input: string, expected: number) {
|
|
66
|
+
const parsed = KAS.unitParse(input).coefficient;
|
|
67
|
+
|
|
68
|
+
return +parsed === expected
|
|
69
|
+
? {pass: true}
|
|
70
|
+
: {
|
|
71
|
+
pass: false,
|
|
72
|
+
message: () => `magnitude of ${input} is not ${expected}`,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("units", () => {
|
|
78
|
+
test.only("simplify expressions with units", () => {
|
|
79
|
+
expect([
|
|
80
|
+
new KAS.Mul(new KAS.Rational(1, 100), new KAS.Unit("cup")),
|
|
81
|
+
new KAS.Mul(new KAS.Rational(1, 400), new KAS.Unit("qt")),
|
|
82
|
+
]).toHaveEqualUnits("1/100 cup = 1 / 400 quart");
|
|
83
|
+
|
|
84
|
+
expect(["10 g", "0.01 kg"]).toParseUnitsAsEqual("10 g = 1 / 100 kg");
|
|
85
|
+
|
|
86
|
+
expect(["9.8 kg m / s^2", "9.8 N"]).toParseUnitsAsEqual(
|
|
87
|
+
"9.8 kg m / s^2 = 9.8 N",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(["9.8 m / s^2", "9.8 N / kg"]).toParseUnitsAsEqual(
|
|
91
|
+
"9.8 m / s^2 = 9.8 N / kg",
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect([
|
|
95
|
+
new KAS.Mul(
|
|
96
|
+
new KAS.Float(9.8),
|
|
97
|
+
new KAS.Unit("m"),
|
|
98
|
+
new KAS.Pow(new KAS.Unit("s"), new KAS.Int(-2)),
|
|
99
|
+
),
|
|
100
|
+
new KAS.Mul(
|
|
101
|
+
new KAS.Float(9.8),
|
|
102
|
+
new KAS.Pow(new KAS.Unit("s"), new KAS.Int(-2)),
|
|
103
|
+
new KAS.Unit("m"),
|
|
104
|
+
),
|
|
105
|
+
]).toHaveEqualUnits("9.8 m / s^2 = 9.8 s^-2 m");
|
|
106
|
+
|
|
107
|
+
expect([
|
|
108
|
+
new KAS.Mul(
|
|
109
|
+
new KAS.Int(50),
|
|
110
|
+
new KAS.Unit("m"),
|
|
111
|
+
new KAS.Pow(new KAS.Unit("m"), new KAS.Int(-1)),
|
|
112
|
+
),
|
|
113
|
+
new KAS.Int(50),
|
|
114
|
+
]).toHaveEqualUnits("50 m / m = 50");
|
|
115
|
+
|
|
116
|
+
// There's a long chain of conversions before this is fully simplified.
|
|
117
|
+
// tsp -> tbsp -> cup -> gal -> L -> m^3
|
|
118
|
+
//
|
|
119
|
+
// 1 tbsp 1 cup 1 gal 3.785 L
|
|
120
|
+
// 1 tsp * ------ * ------- * ------ * -------
|
|
121
|
+
// 3 tsp 16 tbsp 16 cup gal
|
|
122
|
+
expect([
|
|
123
|
+
new KAS.Unit("tsp"),
|
|
124
|
+
new KAS.Mul(
|
|
125
|
+
new KAS.Rational(1, 3),
|
|
126
|
+
new KAS.Rational(1, 16),
|
|
127
|
+
new KAS.Rational(1, 16),
|
|
128
|
+
new KAS.Float(3.785),
|
|
129
|
+
new KAS.Unit("L"),
|
|
130
|
+
),
|
|
131
|
+
]).toHaveEqualUnits("tsp reduces");
|
|
132
|
+
|
|
133
|
+
expect(
|
|
134
|
+
KAS.compare(
|
|
135
|
+
new KAS.Mul(new KAS.Int(50), new KAS.Unit("m")),
|
|
136
|
+
new KAS.Int(50),
|
|
137
|
+
).equal,
|
|
138
|
+
).toBeFalse();
|
|
139
|
+
|
|
140
|
+
expect(["50 m", "50 A"]).not.toParseUnitsAsEqual("50 m != 50 A");
|
|
141
|
+
expect(["5000 mA", "5 A"]).toParseUnitsAsEqual("5000 mA = 5 A");
|
|
142
|
+
expect(["5 mA", "5 A"]).not.toParseUnitsAsEqual("5 mA != 5 A");
|
|
143
|
+
|
|
144
|
+
expect(["9.8 kg m / s^2", "9.8 J"]).not.toParseUnitsAsEqual(
|
|
145
|
+
"9.8 kg m / s^2 != 9.8 J",
|
|
146
|
+
);
|
|
147
|
+
expect(["9.8 kg m / s^2", "9.8 J/m"]).toParseUnitsAsEqual(
|
|
148
|
+
"9.8 kg m / s^2 = 9.8 J/m",
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(["mA", "A"]).not.toHaveTheSameForm("mA !== A");
|
|
152
|
+
expect(["g", "kg"]).not.toHaveTheSameForm("g !== kg");
|
|
153
|
+
expect(["kg m / s^2", "N"]).not.toHaveTheSameForm("kg m / s^2 !== N");
|
|
154
|
+
expect(["m / s^2", "N / kg"]).not.toHaveTheSameForm("m / s^2 !== N / kg");
|
|
155
|
+
|
|
156
|
+
expect(["A", "A"]).toHaveTheSameForm("A === A");
|
|
157
|
+
expect(["kg", "kg"]).toHaveTheSameForm("kg = kg");
|
|
158
|
+
expect(
|
|
159
|
+
["kg m / s^2", "m kg / s^2"],
|
|
160
|
+
).toHaveTheSameForm(
|
|
161
|
+
"kg m / s^2 === m kg / s^2",
|
|
162
|
+
);
|
|
163
|
+
expect(["m / s^2", "m / s^2"]).toHaveTheSameForm("m / s^2 === m / s^2");
|
|
164
|
+
|
|
165
|
+
expect(["2.5 gal", "cup"]).toHaveUnitVariable(new KAS.Int(40));
|
|
166
|
+
|
|
167
|
+
// 1 tbsp 1 cup 1 gal 3785 L 1 m^3 ( 100 cm ) 3
|
|
168
|
+
// 1 tsp * ------ * ------- * ------ * -------- * ------ * (--------)
|
|
169
|
+
// 3 tsp 16 tbsp 16 cup 1000 gal 1000 L ( m )
|
|
170
|
+
expect(["1 tsp", "cm^3"]).toHaveUnitVariable(
|
|
171
|
+
new KAS.Rational(3785, 768),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(["1 dozen qt", "gal"]).toHaveUnitVariable(new KAS.Int(3));
|
|
175
|
+
|
|
176
|
+
expect(["1 day", "sec"]).toHaveUnitVariable(new KAS.Int(60 * 60 * 24));
|
|
177
|
+
|
|
178
|
+
expect(["5 kg m / s^2", "N"]).toHaveUnitVariable(new KAS.Int(5));
|
|
179
|
+
|
|
180
|
+
// This was originally broken due to a scientific notation parsing
|
|
181
|
+
// problem. Leaving it around because it doesn't hurt anything.
|
|
182
|
+
expect(["5 x 10^5 N", "lb"]).toHaveUnitVariable(new KAS.Int(112404));
|
|
183
|
+
|
|
184
|
+
expect("5 x 10^5 N").toHaveMagnitude(500000);
|
|
185
|
+
expect("1.23456 x 10^5 N").toHaveMagnitude(123456);
|
|
186
|
+
expect("3.14 x 10^-2 N").toHaveMagnitude(0.0314);
|
|
187
|
+
});
|
|
188
|
+
});
|
package/src/compare.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import _ from "underscore";
|
|
2
|
+
|
|
3
|
+
// Assumes that both expressions have already been parsed
|
|
4
|
+
// TODO(alex): be able to pass a random() function to compare()
|
|
5
|
+
export const compare = function(expr1, expr2, options) {
|
|
6
|
+
var defaults = {
|
|
7
|
+
form: false, // Check that the two expressions have the same form
|
|
8
|
+
simplify: false, // Check that the second expression is simplified
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/* Options that could be added in the future:
|
|
12
|
+
* - Allow ratios: e.g. 3/1 and 3 should both be accepted for something
|
|
13
|
+
* like slope
|
|
14
|
+
* - Allow student to choose their own variable names
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
if (options !== undefined) {
|
|
18
|
+
// eslint-disable-next-line no-undef
|
|
19
|
+
options = _.extend(defaults, options);
|
|
20
|
+
} else {
|
|
21
|
+
options = defaults;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// TODO(CP-1614): Figure out how to make these messages translatable
|
|
25
|
+
|
|
26
|
+
// Variable check
|
|
27
|
+
var vars = expr1.sameVars(expr2);
|
|
28
|
+
if (!vars.equal) {
|
|
29
|
+
var message;
|
|
30
|
+
if (vars.equalIgnoringCase) {
|
|
31
|
+
message =
|
|
32
|
+
"Check your variables; one or more are using " +
|
|
33
|
+
"the wrong case (upper or lower).";
|
|
34
|
+
} else {
|
|
35
|
+
message =
|
|
36
|
+
"Check your variables; you may have used the wrong " +
|
|
37
|
+
"letter for one or more of them.";
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
equal: false,
|
|
41
|
+
wrongVariableCase: vars.equalIgnoringCase,
|
|
42
|
+
wrongVariableNames: !vars.equalIgnoringCase,
|
|
43
|
+
message: message,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Semantic check
|
|
48
|
+
if (!expr1.compare(expr2)) {
|
|
49
|
+
return {equal: false, message: null};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Syntactic check
|
|
53
|
+
if (options.form && !expr1.sameForm(expr2)) {
|
|
54
|
+
return {
|
|
55
|
+
equal: false,
|
|
56
|
+
message: "Your answer is not in the correct form.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Syntactic check
|
|
61
|
+
if (options.simplify && !expr1.isSimplified()) {
|
|
62
|
+
return {
|
|
63
|
+
equal: false,
|
|
64
|
+
message: "Your answer is not fully expanded and simplified.",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {equal: true, message: null};
|
|
69
|
+
};
|
package/src/index.js
ADDED