Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move scorers: Plotter and Sorter #2113

Merged
merged 3 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import {clockwise} from "../../../util/geometry";
// This file contains helper functions for working with angles.

import type {Coord} from "@khanacademy/perseus";
import type {vec} from "mafs";
import {clockwise} from "./geometry";

// This file contains helper functions for working with angles.
import type {Coord} from "@khanacademy/perseus-core";

export function convertDegreesToRadians(degrees: number): number {
return (degrees / 180) * Math.PI;
}

// Returns a value between -180 and 180, inclusive. The angle is measured
// between the positive x-axis and the given vector.
export function calculateAngleInDegrees([x, y]: vec.Vector2): number {
export function calculateAngleInDegrees([x, y]: Coord): number {
return (Math.atan2(y, x) * 180) / Math.PI;
}

// Converts polar coordinates to cartesian. The th(eta) parameter is in degrees.
export function polar(r: number | vec.Vector2, th: number): vec.Vector2 {
export function polar(r: number | Coord, th: number): Coord {
if (typeof r === "number") {
r = [r, r];
}
Expand All @@ -26,10 +25,7 @@ export function polar(r: number | vec.Vector2, th: number): vec.Vector2 {
// This function calculates the angle between two points and an optional vertex.
// If the vertex is not provided, the angle is measured between the two points.
// This does not account for reflex angles or clockwise position.
export const getAngleFromVertex = (
point: vec.Vector2,
vertex: vec.Vector2,
): number => {
export const getAngleFromVertex = (point: Coord, vertex: Coord): number => {
const x = point[0] - vertex[0];
const y = point[1] - vertex[1];
if (!x && !y) {
Expand Down
62 changes: 62 additions & 0 deletions packages/kmath/src/coefficients.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type {SineCoefficient} from "./geometry";
import type {Coord} from "@khanacademy/perseus-core";

export type NamedSineCoefficient = {
amplitude: number;
angularFrequency: number;
phase: number;
verticalOffset: number;
};

// TODO: there's another, very similar getSinusoidCoefficients function
// they should probably be merged
export function getSinusoidCoefficients(
coords: ReadonlyArray<Coord>,
): SineCoefficient {
// It's assumed that p1 is the root and p2 is the first peak
const p1 = coords[0];
const p2 = coords[1];

// Resulting coefficients are canonical for this sine curve
const amplitude = p2[1] - p1[1];
const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
const phase = p1[0] * angularFrequency;
const verticalOffset = p1[1];

return [amplitude, angularFrequency, phase, verticalOffset];
}

export type QuadraticCoefficient = [number, number, number];

// TODO: there's another, very similar getQuadraticCoefficients function
// they should probably be merged
export function getQuadraticCoefficients(
coords: ReadonlyArray<Coord>,
): QuadraticCoefficient {
const p1 = coords[0];
const p2 = coords[1];
const p3 = coords[2];

const denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]);
if (denom === 0) {
// Many of the callers assume that the return value is always defined.
// @ts-expect-error - TS2322 - Type 'undefined' is not assignable to type 'QuadraticCoefficient'.
return;
}
const a =
(p3[0] * (p2[1] - p1[1]) +
p2[0] * (p1[1] - p3[1]) +
p1[0] * (p3[1] - p2[1])) /
denom;
const b =
(p3[0] * p3[0] * (p1[1] - p2[1]) +
p2[0] * p2[0] * (p3[1] - p1[1]) +
p1[0] * p1[0] * (p2[1] - p3[1])) /
denom;
const c =
(p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] +
p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] +
p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) /
denom;
return [a, b, c];
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
* A collection of geomtry-related utility functions
*/

import {number as knumber, point as kpoint, sum} from "@khanacademy/kmath";
import {
approximateDeepEqual,
approximateEqual,
type Coord,
} from "@khanacademy/perseus-core";
import _ from "underscore";

import Util from "../util";

import type {Coord, Line} from "../interactive2/types";
import {number as knumber, point as kpoint, sum} from "@khanacademy/kmath";

const {eq, deepEq} = Util;
type Line = [Coord, Coord];

// This should really be a readonly tuple of [number, number]
export type Range = [number, number];
Expand All @@ -21,12 +23,9 @@ export type SineCoefficient = [
number, // verticalOffset
];

// a, b, c
export type QuadraticCoefficient = [number, number, number];

// Given a number, return whether it is positive (1), negative (-1), or zero (0)
export function sign(val: number): 0 | 1 | -1 {
if (eq(val, 0)) {
if (approximateEqual(val, 0)) {
return 0;
}
return val > 0 ? 1 : -1;
Expand All @@ -39,7 +38,7 @@ export function ccw(a: Coord, b: Coord, c: Coord): number {
}

export function collinear(a: Coord, b: Coord, c: Coord): boolean {
return eq(ccw(a, b, c), 0);
return approximateEqual(ccw(a, b, c), 0);
}

// Given rect bounding points A and B, whether point C is inside the rect
Expand Down Expand Up @@ -229,15 +228,15 @@ export function similar(
// @ts-expect-error - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
sides = rotate(sides, i);

if (deepEq(angles1, angles)) {
if (approximateDeepEqual(angles1, angles)) {
const sidePairs = _.zip(sides1, sides);

const factors = _.map(sidePairs, function (pair) {
return pair[0] / pair[1];
});

const same = _.all(factors, function (factor) {
return eq(factors[0], factor);
return approximateEqual(factors[0], factor);
});

const congruentEnough = _.all(sidePairs, function (pair) {
Expand Down Expand Up @@ -304,7 +303,7 @@ export function rotate<T>(
}

export function getLineEquation(first: Coord, second: Coord): string {
if (eq(first[0], second[0])) {
if (approximateEqual(first[0], second[0])) {
return "x = " + first[0].toFixed(3);
}
const m = (second[1] - first[1]) / (second[0] - first[0]);
Expand Down
8 changes: 8 additions & 0 deletions packages/kmath/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,13 @@ export * as vector from "./vector";
export * as point from "./point";
export * as line from "./line";
export * as ray from "./ray";
export * as angles from "./angles";
export * as geometry from "./geometry";
export * as coefficients from "./coefficients";

export {default as KhanMath, sum} from "./math";

export type {Range, SineCoefficient} from "./geometry";
export type {NamedSineCoefficient, QuadraticCoefficient} from "./coefficients";

export type * from "./types";
3 changes: 3 additions & 0 deletions packages/kmath/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type {Coord} from "@khanacademy/perseus-core";

export type QuadraticCoords = [Coord, Coord, Coord];
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {getDecimalSeparator} from "@khanacademy/perseus-core";

import {MathFieldActionType} from "../../types";
import {getDecimalSeparator} from "../../utils";
import {mathQuillInstance} from "../input/mathquill-instance";

import handleArrow from "./handle-arrow";
Expand Down
7 changes: 2 additions & 5 deletions packages/math-input/src/components/keypad/button-assets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ asset.
In the future it would be great if these were included from files so that
no copying and pasting is necessary.
*/
import {getDecimalSeparator} from "@khanacademy/perseus-core";
import * as React from "react";

import {DecimalSeparator, getDecimalSeparator} from "../../utils";
import {useMathInputI18n} from "../i18n-context";

import type Key from "../../data/keys";
Expand Down Expand Up @@ -176,10 +176,7 @@ export default function ButtonAsset({id}: Props): React.ReactNode {
case "PERIOD":
// Different locales use different symbols for the decimal separator
// (, vs .)
if (
id === "DECIMAL" &&
getDecimalSeparator(locale) === DecimalSeparator.COMMA
) {
if (id === "DECIMAL" && getDecimalSeparator(locale) !== ".") {
// comma decimal separator
return (
<svg
Expand Down
37 changes: 0 additions & 37 deletions packages/math-input/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,3 @@
export const DecimalSeparator = {
COMMA: ",",
PERIOD: ".",
} as const;

/**
* Get the character used for separating decimals.
*/
export const getDecimalSeparator = (locale: string): string => {
let separator: string = DecimalSeparator.PERIOD;

switch (locale) {
// TODO(somewhatabstract): Remove this when Chrome supports the `ka`
// locale properly.
// https://github.com/formatjs/formatjs/issues/1526#issuecomment-559891201
//
// Supported locales in Chrome:
// https://source.chromium.org/chromium/chromium/src/+/master:third_party/icu/scripts/chrome_ui_languages.list
case "ka":
separator = ",";
break;

default:
const numberWithDecimalSeparator = 1.1;
// TODO(FEI-3647): Update to use .formatToParts() once we no longer have to
// support Safari 12.
const match = new Intl.NumberFormat(locale)
.format(numberWithDecimalSeparator)
// 0x661 is ARABIC-INDIC DIGIT ONE
// 0x6F1 is EXTENDED ARABIC-INDIC DIGIT ONE
.match(/[^\d\u0661\u06F1]/);
separator = match?.[0] ?? ".";
}

return separator === "," ? DecimalSeparator.COMMA : DecimalSeparator.PERIOD;
};

const CDOT_ONLY = [
"az",
"cs",
Expand Down
7 changes: 7 additions & 0 deletions packages/perseus-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@ export type {
Relationship,
} from "./types";
export type {ErrorKind} from "./error/errors";
export type {FunctionTypeMappingKeys} from "./utils/grapher-util";
export type {Coords} from "./utils/grapher-types";

// Careful, `version.ts` uses this function so it _must_ be imported above it
export {addLibraryVersionToPerseusDebug} from "./utils/add-library-version-to-perseus-debug";
export {default as getMatrixSize} from "./utils/get-matrix-size";
export {default as getDecimalSeparator} from "./utils/get-decimal-separator";
export {approximateEqual, approximateDeepEqual} from "./utils/equality";
export {default as deepClone} from "./utils/deep-clone";
export * as GrapherUtil from "./utils/grapher-util";

export {libVersion} from "./version";

Expand Down
25 changes: 25 additions & 0 deletions packages/perseus-core/src/utils/deep-clone.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import deepClone from "./deep-clone";

describe("deepClone", () => {
it("does nothing to a primitive", () => {
expect(deepClone(3)).toBe(3);
});

it("copies an array", () => {
const input = [1, 2, 3];

const result = deepClone(input);

expect(result).toEqual(input);
expect(result).not.toBe(input);
});

it("recursively clones array elements", () => {
const input = [[1]];

const result = deepClone(input);

expect(result).toEqual(input);
expect(result[0]).not.toBe(input[0]);
});
});
18 changes: 18 additions & 0 deletions packages/perseus-core/src/utils/deep-clone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// TODO(benchristel): in the future, we may want to make deepClone work for
// Record<string, Cloneable> as well. Currently, it only does arrays.
type Cloneable =
| null
| undefined
| boolean
| string
| number
| Cloneable[]
| readonly Cloneable[];
function deepClone<T extends Cloneable>(obj: T): T {
if (Array.isArray(obj)) {
return obj.map(deepClone) as T;
}
return obj;
}

export default deepClone;
55 changes: 55 additions & 0 deletions packages/perseus-core/src/utils/equality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import _ from "underscore";

/**
* APPROXIMATE equality on numbers and primitives.
*/
export function approximateEqual<T>(x: T, y: T): boolean {
if (typeof x === "number" && typeof y === "number") {
return Math.abs(x - y) < 1e-9;
}
return x === y;
}

/**
* Deep APPROXIMATE equality on primitives, numbers, arrays, and objects.
* Recursive.
*/
export function approximateDeepEqual<T>(x: T, y: T): boolean {
if (Array.isArray(x) && Array.isArray(y)) {
if (x.length !== y.length) {
return false;
}
for (let i = 0; i < x.length; i++) {
if (!approximateDeepEqual(x[i], y[i])) {
return false;
}
}
return true;
}
if (Array.isArray(x) || Array.isArray(y)) {
return false;
}
if (typeof x === "function" && typeof y === "function") {
return approximateEqual(x, y);
}
if (typeof x === "function" || typeof y === "function") {
return false;
}
if (typeof x === "object" && typeof y === "object" && !!x && !!y) {
return (
x === y ||
(_.all(x, function (v, k) {
// @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
return approximateDeepEqual(y[k], v);
}) &&
_.all(y, function (v, k) {
// @ts-expect-error - TS2536 - Type 'CollectionKey<T>' cannot be used to index type 'T'.
return approximateDeepEqual(x[k], v);
}))
);
}
if ((typeof x === "object" && !!x) || (typeof y === "object" && !!y)) {
return false;
}
return approximateEqual(x, y);
}
Loading
Loading