Skip to content

Commit

Permalink
Move validation types from perseus to perseus-score (#2101)
Browse files Browse the repository at this point in the history
## Summary:
This looks like a lot, but it's basically just moving one file and updating imports.

Issue: LEMS-2737

## Test plan:
- No functionality should change, just moving types

Author: handeyeco

Reviewers: handeyeco, jeremywiebe, Myranae

Required Reviewers:

Approved By: jeremywiebe

Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x)

Pull Request URL: #2101
  • Loading branch information
handeyeco authored Jan 17, 2025
1 parent 41ffd4a commit 9cabe68
Show file tree
Hide file tree
Showing 184 changed files with 1,680 additions and 1,644 deletions.
11 changes: 11 additions & 0 deletions .changeset/quick-spoons-whisper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@khanacademy/perseus": major
"@khanacademy/kmath": minor
"@khanacademy/perseus-core": minor
"@khanacademy/perseus-score": minor
"@khanacademy/perseus-dev-ui": patch
"@khanacademy/math-input": patch
"@khanacademy/perseus-editor": patch
---

Move scorers and validators to `perseus-score`
2 changes: 1 addition & 1 deletion config/test/custom-matchers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Ok here we are

import type {PerseusScore} from "../../packages/perseus/src/types";
import type {PerseusScore} from "../../packages/perseus-score/src/validation.types";

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
Expand Down
4 changes: 3 additions & 1 deletion dev/flipbook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@ import {
} from "./flipbook-model";
import {Header} from "./header";

import type {APIOptions, PerseusScore} from "../packages/perseus/src";
import type {APIOptions} from "../packages/perseus/src";
import type {
InteractiveGraphWidget,
PerseusRenderer,
PerseusWidget,
} from "../packages/perseus-core/src/data-schema";
import type {PerseusScore} from "../packages/perseus-score/src/validation.types";
import type {PropsFor} from "@khanacademy/wonder-blocks-core";

import "../packages/perseus/src/styles/perseus-renderer.less";

const exampleCommands = `
Expand Down
File renamed without changes.
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];
}
File renamed without changes.
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
10 changes: 10 additions & 0 deletions packages/perseus-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@ export type {
KEScore,
KeypadContextRendererInterface,
RendererInterface,
MarkerType,
InteractiveMarkerType,
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
24 changes: 24 additions & 0 deletions packages/perseus-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,27 @@ export type KEScore = {
guess: any;
state: any;
};

// Base marker, with the props that are set by the editor.
export type MarkerType = {
// The list of correct answers expected for the marker.
answers: ReadonlyArray<string>;
// The marker title or description.
label: string;
// The marker coordinates on the question image as percent of image size.
x: number;
y: number;
};

// Additional props that are set when user interacts with the marker.
export type InteractiveMarkerType = MarkerType & {
// The user selected list of answers, used to grade the question.
selected?: ReadonlyArray<string>;
// Reveal the correctness state of the user selected answers for the marker.
showCorrectness?: "correct" | "incorrect";
focused?: boolean;
};

// Used for NumberLine
// TODO: can this be merged with PerseusNumberLineWidgetOptions.correctRel?
export type Relationship = "lt" | "gt" | "le" | "ge";
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;
Loading

0 comments on commit 9cabe68

Please sign in to comment.