@khanacademy/kmath 0.0.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.
@@ -0,0 +1,119 @@
1
+ // @flow
2
+
3
+ import * as line from "../line.js";
4
+
5
+ describe("kline", function () {
6
+ it("finds distance to point", () => {
7
+ const result = line.distanceToPoint(
8
+ [
9
+ [-5, 0],
10
+ [5, 0],
11
+ ],
12
+ [0, 5],
13
+ );
14
+ expect(result).toBe(5);
15
+ });
16
+
17
+ it("reflects a point", () => {
18
+ const result = line.reflectPoint(
19
+ [
20
+ [-5, -5],
21
+ [5, 5],
22
+ ],
23
+ [0, 5],
24
+ );
25
+ expect(result).toEqual([5, 0]);
26
+ });
27
+
28
+ it("finds the midpoint", () => {
29
+ const result = line.midpoint([
30
+ [-5, -5],
31
+ [5, 5],
32
+ ]);
33
+ expect(result).toEqual([0, 0]);
34
+ });
35
+
36
+ it("two identical lines should be equal", function () {
37
+ const result = line.equal(
38
+ [
39
+ [1, 1],
40
+ [3, 3],
41
+ ],
42
+ [
43
+ [1, 1],
44
+ [3, 3],
45
+ ],
46
+ );
47
+ expect(result).toBe(true);
48
+ });
49
+
50
+ it("two parallel lines should not be equal", function () {
51
+ const result = line.equal(
52
+ [
53
+ [1, 1],
54
+ [3, 3],
55
+ ],
56
+ [
57
+ [1, 2],
58
+ [3, 4],
59
+ ],
60
+ );
61
+ expect(result).toBe(false);
62
+ });
63
+
64
+ it("two intersecting lines should not be equal", function () {
65
+ const result = line.equal(
66
+ [
67
+ [1, 1],
68
+ [3, 3],
69
+ ],
70
+ [
71
+ [1, 1],
72
+ [3, 4],
73
+ ],
74
+ );
75
+ expect(result).toBe(false);
76
+ });
77
+
78
+ it("two collinear lines should be equal #1", function () {
79
+ const result = line.equal(
80
+ [
81
+ [1, 1],
82
+ [3, 3],
83
+ ],
84
+ [
85
+ [0, 0],
86
+ [5, 5],
87
+ ],
88
+ );
89
+ expect(result).toBe(true);
90
+ });
91
+
92
+ it("two collinear lines should be equal #2", function () {
93
+ const result = line.equal(
94
+ [
95
+ [4, 4],
96
+ [5, 5],
97
+ ],
98
+ [
99
+ [0, 0],
100
+ [1, 1],
101
+ ],
102
+ );
103
+ expect(result).toBe(true);
104
+ });
105
+
106
+ it("two collinear lines should be equal #3", function () {
107
+ const result = line.equal(
108
+ [
109
+ [0, 0],
110
+ [1, 1],
111
+ ],
112
+ [
113
+ [3, 3],
114
+ [6, 6],
115
+ ],
116
+ );
117
+ expect(result).toBe(true);
118
+ });
119
+ });
@@ -0,0 +1,119 @@
1
+ // @flow
2
+
3
+ import * as number from "../number.js";
4
+
5
+ describe("knumber", function () {
6
+ it.each([3, Math.PI, 6.28, 5e10, 1 / 0])("is a number: %s", (num) => {
7
+ expect(number.is(num)).toBe(true);
8
+ });
9
+
10
+ it.each(["10", 0 / 0, NaN])("is not a number:%s", (num) => {
11
+ expect(number.is(num)).toBe(false);
12
+ });
13
+
14
+ it("two equal numbers should be equal", function () {
15
+ const result = number.equal(1 / 3, (1 / 90) * 30);
16
+ expect(result).toBe(true);
17
+ });
18
+
19
+ it("two different numbers should not be equal", function () {
20
+ const result = number.equal(1 / 3, 1.333333);
21
+ expect(result).toBe(false);
22
+ });
23
+
24
+ it("Infinity should equal Infinity", function () {
25
+ const result = number.equal(
26
+ Number.POSITIVE_INFINITY,
27
+ Number.POSITIVE_INFINITY,
28
+ );
29
+ expect(result).toBe(true);
30
+ });
31
+
32
+ it("+Infinity should not equal -Infinity", function () {
33
+ const result = number.equal(
34
+ Number.POSITIVE_INFINITY,
35
+ Number.NEGATIVE_INFINITY,
36
+ );
37
+ expect(result).toBe(false);
38
+ });
39
+
40
+ it("sign(0) should be 0", function () {
41
+ expect(number.sign(0)).toBe(0);
42
+ });
43
+
44
+ it("sign(-0.0) should be 0", function () {
45
+ expect(number.sign(-0.0)).toBe(0);
46
+ });
47
+
48
+ it("sign(3.2) should be 1", function () {
49
+ expect(number.sign(3.2)).toBe(1);
50
+ });
51
+
52
+ it("sign(-2.8) should be -1", function () {
53
+ expect(number.sign(-2.8)).toBe(-1);
54
+ });
55
+
56
+ it("isInteger(-2.8) should be false", function () {
57
+ expect(number.isInteger(-2.8)).toBe(false);
58
+ });
59
+
60
+ it("isInteger(-2) should be true", function () {
61
+ expect(number.isInteger(-2)).toBe(true);
62
+ });
63
+
64
+ it("isInteger(10.0) should be true", () => {
65
+ expect(number.isInteger(10)).toBe(true);
66
+ });
67
+
68
+ it("rounds to correct precision", () => {
69
+ expect(number.round(0.06793, 4)).toBe(0.0679);
70
+ expect(number.round(0.06793, 3)).toBe(0.068);
71
+ });
72
+
73
+ it("rounds to correct interval", () => {
74
+ expect(number.roundTo(83, 5)).toBe(85);
75
+ expect(number.roundTo(2.3, 0.5)).toBe(2.5);
76
+ });
77
+
78
+ it("floors to the correct interval", () => {
79
+ expect(number.floorTo(83, 5)).toBe(80);
80
+ expect(number.floorTo(2.3, 0.5)).toBe(2);
81
+ });
82
+
83
+ it("ceils to the correct interval", () => {
84
+ expect(number.ceilTo(81, 5)).toBe(85);
85
+ expect(number.ceilTo(2.1, 0.5)).toBe(2.5);
86
+ });
87
+
88
+ it("toFraction(-2) should be -2/1", function () {
89
+ expect(number.toFraction(-2)).toStrictEqual([-2, 1]);
90
+ });
91
+
92
+ it("toFraction(-2.5) should be -5/2", function () {
93
+ expect(number.toFraction(-2.5)).toStrictEqual([-5, 2]);
94
+ });
95
+
96
+ it("toFraction(2/3) should be 2/3", function () {
97
+ expect(number.toFraction(2 / 3)).toStrictEqual([2, 3]);
98
+ });
99
+
100
+ it("toFraction(283.33...) should be 850/3", function () {
101
+ expect(number.toFraction(283 + 1 / 3)).toStrictEqual([850, 3]);
102
+ });
103
+
104
+ it("toFraction(0) should be 0/1", function () {
105
+ expect(number.toFraction(0)).toStrictEqual([0, 1]);
106
+ });
107
+
108
+ it("toFraction(pi) should be pi/1", function () {
109
+ expect(number.toFraction(Math.PI)).toStrictEqual([Math.PI, 1]);
110
+ });
111
+
112
+ it("toFraction(0.66) should be 33/50", function () {
113
+ expect(number.toFraction(0.66)).toStrictEqual([33, 50]);
114
+ });
115
+
116
+ it("toFraction(0.66, 0.01) should be 2/3", function () {
117
+ expect(number.toFraction(0.66, 0.01)).toStrictEqual([2, 3]);
118
+ });
119
+ });
@@ -0,0 +1,50 @@
1
+ // @flow
2
+
3
+ import * as point from "../point.js";
4
+
5
+ describe("kpoint", function () {
6
+ it("point.compare should return positive if the first element is larger", function () {
7
+ const result = point.compare([5, 2], [3, 4]);
8
+ expect(result).toBeGreaterThan(0);
9
+ });
10
+
11
+ it("point.compare should return negative if the first element is smaller", function () {
12
+ const result = point.compare([2, 2], [4, 0]);
13
+ expect(result).toBeLessThan(0);
14
+ });
15
+
16
+ it("point.compare should return positive if the second element is larger", function () {
17
+ const result = point.compare([5, 2], [5, 1]);
18
+ expect(result).toBeGreaterThan(0);
19
+ });
20
+
21
+ it("point.compare should return negative if the second element is smaller", function () {
22
+ const result = point.compare([2, 2], [2, 4]);
23
+ expect(result).toBeLessThan(0);
24
+ });
25
+
26
+ it("point.compare should return positive if the third element is larger", function () {
27
+ const result = point.compare([5, 3, -2], [5, 3, -4]);
28
+ expect(result).toBeGreaterThan(0);
29
+ });
30
+
31
+ it("point.compare should return negative if the third element is smaller", function () {
32
+ const result = point.compare([2, -1, -4], [2, -1, -2]);
33
+ expect(result).toBeLessThan(0);
34
+ });
35
+
36
+ it("point.compare should return 0 if the vectors are equal", function () {
37
+ const result = point.compare([2, 4, 3], [2, 4, 3]);
38
+ expect(result).toBe(0);
39
+ });
40
+
41
+ it("point.compare should return negative if v1 is shorter than v2", function () {
42
+ const result = point.compare([2, 4], [2, 4, 3]);
43
+ expect(result).toBeLessThan(0);
44
+ });
45
+
46
+ it("point.compare should return positive if v1 is longer than v2", function () {
47
+ const result = point.compare([2, 4, -2], [2, 2]);
48
+ expect(result).toBeGreaterThan(0);
49
+ });
50
+ });
@@ -0,0 +1,113 @@
1
+ // @flow
2
+
3
+ import * as vector from "../vector.js";
4
+
5
+ describe("kvector", function () {
6
+ it("vector.add should add two 2D vectors", function () {
7
+ const result = vector.add([1, 2], [3, 4]);
8
+ expect(result).toStrictEqual([4, 6]);
9
+ });
10
+
11
+ it("vector.add should add two 3D vectors", function () {
12
+ const result = vector.add([1, 2, 3], [4, 5, 6]);
13
+ expect(result).toStrictEqual([5, 7, 9]);
14
+ });
15
+
16
+ it("vector.add should add three 2D vectors", function () {
17
+ const result = vector.add([1, 2], [3, 4], [5, 6]);
18
+ expect(result).toStrictEqual([9, 12]);
19
+ });
20
+
21
+ it("vector.subtract should subtract two 2D vectors", function () {
22
+ const result = vector.subtract([1, 2], [3, 4]);
23
+ expect(result).toStrictEqual([-2, -2]);
24
+ });
25
+
26
+ it("vector.subtract should subtract two 3D vectors", function () {
27
+ const result = vector.subtract([1, 2, 3], [4, 5, 6]);
28
+ expect(result).toStrictEqual([-3, -3, -3]);
29
+ });
30
+
31
+ it("vector.dot should take the dot product of 2 2D vectors", function () {
32
+ const result = vector.dot([1, 2], [3, 4]);
33
+ expect(result).toBe(3 + 8);
34
+ });
35
+
36
+ it("vector.dot should take the dot product of 2 3D vectors", function () {
37
+ const result = vector.dot([1, 2, 3], [4, 5, 6]);
38
+ expect(result).toBe(4 + 10 + 18);
39
+ });
40
+
41
+ it("vector.scale should scale a 2D vector", function () {
42
+ const result = vector.scale([4, 2], 0.5);
43
+ expect(result).toStrictEqual([2, 1]);
44
+ });
45
+
46
+ it("vector.scale should scale a 3D vector", function () {
47
+ const result = vector.scale([1, 2, 3], 2);
48
+ expect(result).toStrictEqual([2, 4, 6]);
49
+ });
50
+
51
+ it("vector.length should take the length of a 2D vector", function () {
52
+ const result = vector.length([3, 4]);
53
+ expect(result).toBe(5);
54
+ });
55
+
56
+ it("vector.length should take the length of a 3D vector", function () {
57
+ const result = vector.length([4, 0, 3]);
58
+ expect(result).toBe(5);
59
+ });
60
+
61
+ it("vector.equal should return true on two equal 3D vectors", function () {
62
+ const result = vector.equal([6, 3, 4], [6, 3, 4]);
63
+ expect(result).toBe(true);
64
+ });
65
+
66
+ it("vector.equal should return false on two inequal 3D vectors", function () {
67
+ const result = vector.equal([6, 3, 4], [6, 4, 4]);
68
+ expect(result).toBe(false);
69
+ });
70
+
71
+ it("vector.equal should return false on a 2D and 3D vector", function () {
72
+ const result = vector.equal([6, 4], [6, 4, 4]);
73
+ expect(result).toBe(false);
74
+ });
75
+
76
+ it("vector.equal should return false on a 2D and 3D vector", function () {
77
+ const result = vector.equal([6, 3, 4], [6, 3]);
78
+ expect(result).toBe(false);
79
+ });
80
+
81
+ it("vector.equal should return false on a 2D and 3D vector with a trailing 0", function () {
82
+ const result = vector.equal([6, 3, 0], [6, 3]);
83
+ expect(result).toBe(false);
84
+ });
85
+
86
+ it(
87
+ "vector.collinear should return true on two collinear vectors of " +
88
+ "the same magnitude but different direction",
89
+ function () {
90
+ const result = vector.collinear([3, 3], [-3, -3]);
91
+ expect(result).toBe(true);
92
+ },
93
+ );
94
+
95
+ it(
96
+ "vector.collinear should return true on two collinear vectors of " +
97
+ "different magnitudes",
98
+ function () {
99
+ const result = vector.collinear([2, 1], [6, 3]);
100
+ expect(result).toBe(true);
101
+ },
102
+ );
103
+
104
+ it("vector.collinear should return false on non-collinear vectors", function () {
105
+ const result = vector.collinear([1, 2], [-1, 2]);
106
+ expect(result).toBe(false);
107
+ });
108
+
109
+ it("vector.negate of [-2, 2] is [2, -2]", function () {
110
+ const result = vector.negate([-2, 2]);
111
+ expect(result).toStrictEqual([2, -2]);
112
+ });
113
+ });
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // @flow
2
+
3
+ export * as number from "./number.js";
4
+ export * as vector from "./vector.js";
5
+ export * as point from "./point.js";
6
+ export * as line from "./line.js";
7
+ export * as ray from "./ray.js";
package/src/line.js ADDED
@@ -0,0 +1,46 @@
1
+ // @flow
2
+ /**
3
+ * Line Utils
4
+ * A line is an array of two points e.g. [[-5, 0], [5, 0]].
5
+ */
6
+
7
+ import * as kpoint from "./point.js";
8
+ import * as kvector from "./vector.js";
9
+
10
+ import type {Point} from "./point.js";
11
+
12
+ export type Line = [Point, Point];
13
+
14
+ export function distanceToPoint(line: Line, point: Point): number {
15
+ return kpoint.distanceToLine(point, line);
16
+ }
17
+
18
+ export function reflectPoint(
19
+ line: Line,
20
+ point: Point,
21
+ ): $ReadOnlyArray<number> /* TODO: convert to Point */ {
22
+ return kpoint.reflectOverLine(point, line);
23
+ }
24
+
25
+ export function midpoint(line: Line): Point {
26
+ return [(line[0][0] + line[1][0]) / 2, (line[0][1] + line[1][1]) / 2];
27
+ }
28
+
29
+ export function equal(line1: Line, line2: Line, tolerance?: number): boolean {
30
+ // TODO: A nicer implementation might just check collinearity of
31
+ // vectors using underscore magick
32
+ // Compare the directions of the lines
33
+ const v1 = kvector.subtract(line1[1], line1[0]);
34
+ const v2 = kvector.subtract(line2[1], line2[0]);
35
+ if (!kvector.collinear(v1, v2, tolerance)) {
36
+ return false;
37
+ }
38
+ // If the start point is the same for the two lines, then they are the same
39
+ if (kpoint.equal(line1[0], line2[0])) {
40
+ return true;
41
+ }
42
+ // Make sure that the direction to get from line1 to
43
+ // line2 is the same as the direction of the lines
44
+ const line1ToLine2Vector = kvector.subtract(line2[0], line1[0]);
45
+ return kvector.collinear(v1, line1ToLine2Vector, tolerance);
46
+ }
package/src/logo.js ADDED
@@ -0,0 +1,41 @@
1
+ // This file describes the graphie source code of the kmath logo
2
+ // currently used on khan.github.io.
3
+ //
4
+ // Also located at http://ka-perseus-graphie.s3.amazonaws.com/42ef3cbadc3e6464124533191728c3c5c55c7355.svg
5
+
6
+ // eslint-disable-next-line flowtype/no-types-missing-file-annotation
7
+ declare var init: $FlowFixMe;
8
+ // eslint-disable-next-line flowtype/no-types-missing-file-annotation
9
+ declare var ellipse: $FlowFixMe;
10
+ // eslint-disable-next-line flowtype/no-types-missing-file-annotation
11
+ declare var line: $FlowFixMe;
12
+
13
+ const GREEN = "#28AE7B";
14
+
15
+ export default () => {
16
+ init({
17
+ range: [
18
+ [0, 10],
19
+ [0, 10],
20
+ ],
21
+ scale: 40,
22
+ });
23
+
24
+ ellipse(5, 5, 5, {
25
+ stroke: null,
26
+ fill: GREEN,
27
+ });
28
+
29
+ line([2, 5], [8.5, 5], {
30
+ stroke: "WHITE",
31
+ fill: "WHITE",
32
+ strokeWidth: 25,
33
+ arrows: "->",
34
+ });
35
+
36
+ line([5, 2], [5, 8], {
37
+ stroke: "WHITE",
38
+ fill: "WHITE",
39
+ strokeWidth: 25,
40
+ });
41
+ };
package/src/number.js ADDED
@@ -0,0 +1,104 @@
1
+ // @flow
2
+ /**
3
+ * Number Utils
4
+ * A number is a js-number, e.g. 5.12
5
+ */
6
+
7
+ import _ from "underscore";
8
+
9
+ export const DEFAULT_TOLERANCE: number = 1e-9;
10
+
11
+ // TODO: Should this just be Number.Epsilon
12
+ export const EPSILON: number = Math.pow(2, -42);
13
+
14
+ export function is(x: any): boolean {
15
+ return _.isNumber(x) && !_.isNaN(x);
16
+ }
17
+
18
+ export function equal(x: number, y: number, tolerance?: number): boolean {
19
+ // Checking for undefined makes this function behave nicely
20
+ // with vectors of different lengths that are _.zip'd together
21
+ if (x == null || y == null) {
22
+ return x === y;
23
+ }
24
+ // We check === here so that +/-Infinity comparisons work correctly
25
+ if (x === y) {
26
+ return true;
27
+ }
28
+ if (tolerance == null) {
29
+ tolerance = DEFAULT_TOLERANCE;
30
+ }
31
+ return Math.abs(x - y) < tolerance;
32
+ }
33
+
34
+ export function sign(
35
+ x: number,
36
+ tolerance?: number,
37
+ ): number /* Should be: 0 | 1 | -1 */ {
38
+ return equal(x, 0, tolerance) ? 0 : Math.abs(x) / x;
39
+ }
40
+
41
+ export function isInteger(num: number, tolerance?: number): boolean {
42
+ return equal(Math.round(num), num, tolerance);
43
+ }
44
+
45
+ // Round a number to a certain number of decimal places
46
+ export function round(num: number, precision: number): number {
47
+ const factor = Math.pow(10, precision);
48
+ return Math.round(num * factor) / factor;
49
+ }
50
+
51
+ // Round num to the nearest multiple of increment
52
+ // i.e. roundTo(83, 5) -> 85
53
+ export function roundTo(num: number, increment: number): number {
54
+ return Math.round(num / increment) * increment;
55
+ }
56
+
57
+ export function floorTo(num: number, increment: number): number {
58
+ return Math.floor(num / increment) * increment;
59
+ }
60
+
61
+ export function ceilTo(num: number, increment: number): number {
62
+ return Math.ceil(num / increment) * increment;
63
+ }
64
+
65
+ /**
66
+ * toFraction
67
+ *
68
+ * Returns a [numerator, denominator] array rational representation
69
+ * of `decimal`
70
+ *
71
+ * See http://en.wikipedia.org/wiki/Continued_fraction for implementation
72
+ * details
73
+ *
74
+ * toFraction(4/8) => [1, 2]
75
+ * toFraction(0.66) => [33, 50]
76
+ * toFraction(0.66, 0.01) => [2/3]
77
+ * toFraction(283 + 1/3) => [850, 3]
78
+ */
79
+ export function toFraction(
80
+ decimal: number,
81
+ tolerance: number = EPSILON, // can't be 0
82
+ maxDenominator: number = 1000,
83
+ ): [number, number] {
84
+ // Initialize everything to compute successive terms of
85
+ // continued-fraction approximations via recurrence relation
86
+ let n = [1, 0];
87
+ let d = [0, 1];
88
+ let a = Math.floor(decimal);
89
+ let rem = decimal - a;
90
+
91
+ while (d[0] <= maxDenominator) {
92
+ if (equal(n[0] / d[0], decimal, tolerance)) {
93
+ return [n[0], d[0]];
94
+ }
95
+ n = [a * n[0] + n[1], n[0]];
96
+ d = [a * d[0] + d[1], d[0]];
97
+ a = Math.floor(1 / rem);
98
+ rem = 1 / rem - a;
99
+ }
100
+
101
+ // We failed to find a nice rational representation,
102
+ // so return an irrational "fraction"
103
+ return [decimal, 1];
104
+ }