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

Point #8

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
13 changes: 7 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { parseMultiple, Formula, Presets } from "./multiline"
export { UniqASTNode, extractVariables } from './ast'
export { epsilon } from './multiline'
export { texToPlain } from './tex'
import type { RangeResultType } from './util'
import { RangeResults, RangeResultType } from './util'
export {
RangeResults,
CompareMode,
RangeResultType,
} from './util'

export type RangeMinMaxResult = [status: RangeResultType, min: number, max: number] | typeof RangeResults.EQNAN
export type RangeFunction2D = (xmin: number, xmax: number, ymin: number, ymax: number) => RangeResultType
export type RangeFunction3D = (xmin: number, xmax: number, ymin: number, ymax: number, zmin: number, zmax: number) => RangeResultType
export type ValueFunction2D = (x: number, y: number) => number
Expand All @@ -22,12 +23,12 @@ export {
astToValueFunctionCode,
} from "./multiline"

export function parse(expression: string, argNames: string[], presets: Presets): Formula
export function parse(expressions: string[], argNames: string[], presets: Presets): Formula[]
export function parse(expressionOrExpressions: string | string[], argNames: string[], presets: Presets): Formula | Formula[] {
export function parse(expression: string, argNames: string[], overridableArgNames: string[], presets: Presets): Formula
export function parse(expressions: string[], argNames: string[], overridableArgNames: string[], presets: Presets): Formula[]
export function parse(expressionOrExpressions: string | string[], argNames: string[], overridableArgNames: string[], presets: Presets): Formula | Formula[] {
if (Array.isArray(expressionOrExpressions)) {
return parseMultiple(expressionOrExpressions, argNames, presets)
return parseMultiple(expressionOrExpressions, argNames, overridableArgNames, presets)
} else {
return parseMultiple([expressionOrExpressions], argNames, presets)[0]
return parseMultiple([expressionOrExpressions], argNames, overridableArgNames, presets)[0]
}
}
47 changes: 36 additions & 11 deletions src/multiline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ export type Presets = Record<string, string | number | PresetFunc>
type VarDef = { type: 'var'; name: string; deps: string[]; ast: UniqASTNode | null; error?: string }
type FuncDef = { type: 'func'; name: string; deps: string[]; args: string[]; ast: UniqASTNode | null; error?: string }
type Equation = { type: 'eq'; mode: CompareMode; deps: string[]; ast: UniqASTNode | null; error?: string }
type Point = { type: 'point'; deps: string[]; axis: UniqASTNode[] | null; error?: string }
type Definition = VarDef | FuncDef
export type Formula = Definition | Equation
export type Formula = Definition | Equation | Point
export const epsilon = 1e-15

const partials: Record<string, string> = { ...factorialPartials }
Expand All @@ -27,10 +28,10 @@ function embedRequiredPartials(code: string) {
return [...requiredCodes, trimmed].join(';')
}

export function parseMultiple(formulaTexts: string[], argNames: string[], presets?: Presets) {
export function parseMultiple(formulaTexts: string[], argNames: string[], overridableArgNames: string[], presets: Presets) {
const uniq = new UniqASTGenerator()
const predefinedVars = new Set(argNames)
const varNames = new Set(predefinedVars)
const varNames = new Set([...predefinedVars, ...overridableArgNames])
const varDefRegexp = /^ *([a-zA-Zα-ωΑ-Ω]+) *(\( *[a-zA-Zα-ωΑ-Ω]+(?: *, *[a-zA-Zα-ωΑ-Ω]+)* *\))? *=(.*)/
const funcNames = new Set(predefinedFunctionNames)
presets = { ...defaultPresets, ...presets }
Expand Down Expand Up @@ -65,6 +66,8 @@ export function parseMultiple(formulaTexts: string[], argNames: string[], preset
} else {
try {
const [ast, mode] = parse(body, varNames, funcNames)
if (mode != null) throw 'Unexpected compare operator'
if (Array.isArray(ast)) throw 'Unexpected point'
const deps = extractVariables(ast)
definition = { type: 'var', name, deps, ast: uniq.convert(ast) }
} catch(e) {
Expand All @@ -78,7 +81,8 @@ export function parseMultiple(formulaTexts: string[], argNames: string[], preset
let definition: FuncDef
try {
const [ast, mode] = parse(body, new Set([...varNames, ...args]), funcNames)
if (mode != null) throw `Unexpected compare operator`
if (mode != null) throw 'Unexpected compare operator'
if (Array.isArray(ast)) throw 'Unexpected point'
const duplicateArgs = duplicates(args)
if (duplicateArgs.length !== 0) throw `Duplicated argument name: ${JSON.stringify(duplicateArgs)}`
const variables = extractVariables(ast).filter(n => !args.includes(n))
Expand Down Expand Up @@ -108,8 +112,13 @@ export function parseMultiple(formulaTexts: string[], argNames: string[], preset
if (!match || !name || !keywordsSet.has(name) || vars.has(name) || funcs.has(name) || predefinedVars.has(name) || predefinedFunctionNames.has(name)) {
try {
const [ast, mode] = parse(f, varNames, funcNames)
const deps = extractVariables(ast)
return { type: 'eq', mode, deps, ast: uniq.convert(ast) }
if (Array.isArray(ast)) {
const deps = [...new Set(...ast.flatMap(axis => extractVariables(axis)))]
return { type: 'point', deps, axis: ast.map(axis => uniq.convert(axis)) }
} else {
const deps = extractVariables(ast)
return { type: 'eq', mode, deps, ast: uniq.convert(ast) }
}
} catch (e) {
return { type: 'eq', mode: null, deps: [], ast: null, error: String(e) }
}
Expand All @@ -127,6 +136,15 @@ export function parseMultiple(formulaTexts: string[], argNames: string[], preset
recursiveCheck(formulas, defs)
const preEvaluateResults = new Map<UniqASTNode, UniqASTNode>()
return formulas.map(f => {
if (f.type === 'point') {
if (!f.axis || f.error) return f
try {
const expandedAxis = f.axis.map(axis => preEvaluateAST(expandAST(axis, vars, funcs, uniq), uniq, preEvaluateResults))
return { ...f, axis: expandedAxis }
} catch(e) {
return { ...f, axis: null, error: String(e) }
}
}
if (!f.ast || f.error) return f
if (f.type === 'func') return f
try {
Expand Down Expand Up @@ -179,7 +197,7 @@ function recursiveCheck(formulas: Formula[], defs: Map<string, Definition>) {
const rec = new Set<string>()
function check(formula: Formula) {
if (formula.error) return
if (formula.type !== 'eq') {
if (formula.type === 'var' || formula.type === 'func') {
if (rec.has(formula.name)) {
formula.error = `cannot define recursively: ${formula.name}`
formula.ast = null
Expand All @@ -194,9 +212,13 @@ function recursiveCheck(formulas: Formula[], defs: Map<string, Definition>) {
const errorDep = formula.deps.find(n => defs.get(n)?.error)
if (errorDep) {
formula.error = formula.error || `${errorDep} is not defined`
formula.ast = null
if (formula.type === 'point') {
formula.axis = null
} else {
formula.ast = null
}
}
if (formula.type !== 'eq') rec.delete(formula.name)
if (formula.type === 'var' || formula.type === 'func') rec.delete(formula.name)
}
for (const f of formulas) check(f)
}
Expand Down Expand Up @@ -295,7 +317,7 @@ export function astToValueFunctionCode(uniqAST: UniqASTNode, args: string[]) {
return embedRequiredPartials(`(${args.join(',')})=>{${codes.join('\n')}\nreturn ${rcode}}`)
}

export function astToRangeFunctionCode(uniqAST: UniqASTNode, args: string[], option: { pos?: boolean; neg?: boolean; zero?: boolean; eq?: boolean }) {
export function astToRangeFunctionCode(uniqAST: UniqASTNode, args: string[], option: { pos?: boolean; neg?: boolean; zero?: boolean; eq?: boolean; range?: false } | { pos?: false; neg?: false; zero?: false; eq?: false; range: true }) {
const [tempVars, returnAST] = toProcedure(uniqAST)
const namer = createNameGenerator()
const vars: Record<string, MinMaxVarName> = {}
Expand All @@ -320,6 +342,7 @@ export function astToRangeFunctionCode(uniqAST: UniqASTNode, args: string[], opt
const argsPart = `(${args.map(a => `${a}min,${a}max`).join(',')})`
if (typeof result === 'number') {
const val = isNaN(result) ? RangeResults.EQNAN : result < -epsilon ? RangeResults.NEGATIVE : result > epsilon ? RangeResults.POSITIVE : RangeResults.EQZERO
if (option.range) return `${argsPart}=>[${val},${result},${result}]`
return `${argsPart}=>${val}`
}
const fullCode = [...codes, rcode].join('\n')
Expand All @@ -336,7 +359,9 @@ export function astToRangeFunctionCode(uniqAST: UniqASTNode, args: string[], opt
const zeroRetPartWithNaN = option.zero ? `${minvar}>-${epsilon}&&${maxvar}<${epsilon}?${nanRetPart}${RangeResults.EQZERO}:` : ''
const cmpEpsilon = option.eq ? -epsilon : epsilon
let returnPart: string
if (option.pos && option.neg) {
if (option.range) {
returnPart = `return [${nanRetPart}${gapRetPart}${RangeResults.OTHER},${minvar},${maxvar}]`
} else if (option.pos && option.neg) {
returnPart = `return ${nanRetPart}${minvar}>${epsilon}?${RangeResults.POSITIVE}:${maxvar}<${-epsilon}?${RangeResults.NEGATIVE}:${zeroRetPart}${gapRetPart}${RangeResults.BOTH}`
} else if (option.pos) {
returnPart = `return ${minvar}>${cmpEpsilon}?${nanRetPart}${RangeResults.POSITIVE}:${maxvar}<${cmpEpsilon}?${RangeResults.OTHER}:${gapRetPart}${RangeResults.BOTH}`
Expand Down
6 changes: 5 additions & 1 deletion src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,13 @@ function flipComparator(cmp: CompareMode): CompareMode {
}
}

function buildRootAST(group: TokenParenGroup, functionNames: Set<string>): [ASTNode, CompareMode] {
function buildRootAST(group: TokenParenGroup, functionNames: Set<string>): [ASTNode, CompareMode] | [ASTNode[], null] {
const idx = group.findIndex(item => typeof item === 'string' && comparers.has(item))
if (idx === -1) {
if (group.length === 1 && Array.isArray(group[0]) && group[0].length >= 2) {
const axis = buildAST(group[0], functionNames)
if (Array.isArray(axis)) return [axis, null] // Point
}
const ast = buildAST(group, functionNames)
if (Array.isArray(ast)) throw 'Unexpected comma'
return [ast, null]
Expand Down