1010import java .util .List ;
1111
1212/**
13- * Reads the calling-convention arguments (t0, y0, tEnd, h, method, points) off
14- * a diffeqn/diffeqnPath/diffeqnHO/diffeqnPathHO call — t0/tEnd/h/method/ points
15- * from {@link Token#getRawArgs()} text (the pragmatic path settled on over
16- * re-deriving the same information structurally, per the explicit "if too
17- * difficult, get it from token.getRawArgs()" fallback), but y0 from the real
18- * compiled {@code Token}, since a bracketed vector literal like
19- * {@code (1, 0, 0, 0, 0)} compiles to a single {@code MATRIX}-kind token named
20- * {@code anonN} rather than staying literal text — re-splitting rawArgs text on
21- * commas breaks the moment y0 contains any expression with its own nested comma
22- * (a function call, another vector), and doesn't reflect how ParserNG actually
13+ * Reads the calling-convention arguments (t0, y0, tEnd, h, method, points,
14+ * presentationStrategy) off a diffeqn/diffeqnPath/diffeqnHO/diffeqnPathHO
15+ * call — t0/tEnd/h/method/points/presentationStrategy from {@link
16+ * Token#getRawArgs()} text (the pragmatic path settled on over re-deriving
17+ * the same information structurally, per the explicit "if too difficult, get
18+ * it from token.getRawArgs()" fallback), but y0 from the real compiled
19+ * {@code Token}, since a bracketed vector literal like {@code (1, 0, 0, 0,
20+ * 0)} compiles to a single {@code MATRIX}-kind token named {@code anonN}
21+ * rather than staying literal text — re-splitting rawArgs text on commas
22+ * breaks the moment y0 contains any expression with its own nested comma (a
23+ * function call, another vector), and doesn't reflect how ParserNG actually
2324 * represents it. The real values are read via
2425 * {@code FunctionManager.lookUp(name).getMatrix().getFlatArray()}. Bracket
2526 * text-splitting is kept only as a last-resort fallback for a shape that
3637 * <h2>Positional argument layout</h2>
3738 * <pre>
3839 * diffeqn: [equation, t0, y0, tEnd, h?, method?]
39- * diffeqnPath: [equation, t0, y0, tEnd, h?, method?, points?]
40- * diffeqnHO: [equation, t0, y0, tEnd, h?, method?] (y0 always a vector here)
41- * diffeqnPathHO: [equation, t0, y0, tEnd, h?, method?, points?]
42- * </pre> h and method are optional per the calling convention; when omitted
43- * this class applies a documented default ({@link #DEFAULT_H}, {@link
44- * #DEFAULT_METHOD}) rather than silently guessing something else — a caller who
45- * cares about the exact solver behavior should always pass both.
40+ * diffeqnPath: [equation, t0, y0, tEnd, h?, method?, points?, presentationStrategy?]
41+ * diffeqnHO: [equation, t0, y0, tEnd, h?, method?] (y0 always a vector here)
42+ * diffeqnPathHO: [equation, t0, y0, tEnd, h?, method?, points?, presentationStrategy?]
43+ * </pre>
44+ * h and method are optional for every kind; points and presentationStrategy
45+ * are additionally available on the *_PATH kinds only, and each is
46+ * INDEPENDENTLY optional — a call may supply neither, just points, just
47+ * presentationStrategy, or both. Because of that, the trailing argument at
48+ * index 6 is disambiguated by content rather than assumed to always be
49+ * points: if it parses as a number it's points (and index 7, if present, is
50+ * then presentationStrategy); if it doesn't parse as a number it's read as
51+ * presentationStrategy directly, and points is left at its default. This is
52+ * what lets {@code diffeqnPathHO(eqn, t0, y0, tEnd, h, method, "state")} work
53+ * — points omitted, presentationStrategy supplied — without the parser
54+ * trying (and failing) to read "state" as a number.
55+ *
56+ * All optional arguments fall back to a documented default ({@link
57+ * #DEFAULT_H}, {@link #DEFAULT_METHOD}, {@link
58+ * #DEFAULT_PRESENTATION_STRATEGY}) when omitted, rather than silently
59+ * guessing something else — a caller who cares about the exact solver
60+ * behavior should always pass them all explicitly. presentationStrategy is
61+ * currently only consumed downstream by diffeqnPathHO; diffeqnPath accepts
62+ * and parses it for forward compatibility, but nothing reads it yet for that
63+ * kind.
4664 */
4765public final class DiffEqnArgParser {
4866
@@ -57,6 +75,12 @@ public final class DiffEqnArgParser {
5775 public static final ODESolverMethod DEFAULT_METHOD
5876 = ODESolverMethod .RK4 ;
5977
78+ /**
79+ * Applied when the call omits the optional presentationStrategy argument.
80+ */
81+ public static final PresentationStrategy DEFAULT_PRESENTATION_STRATEGY
82+ = PresentationStrategy .TRAJECTORY ;
83+
6084 private DiffEqnArgParser () {
6185 }
6286
@@ -107,14 +131,41 @@ public static DiffEqnCall parse(Token[] fullCallPostfix) {
107131 ODESolverMethod method = raw .length > 5 && !raw [5 ].isEmpty ()
108132 ? parseMethod (raw [5 ]) : DEFAULT_METHOD ;
109133
110- int points = -1 ;
111134 boolean pathVariant = kind == DiffEqnCall .Kind .DIFFEQN_PATH || kind == DiffEqnCall .Kind .DIFFEQN_PATH_HO ;
112- if (pathVariant && raw .length > 6 && !raw [6 ].isEmpty ()) {
113- points = (int ) parseDouble (raw [6 ], "points" );
135+
136+ int points = -1 ;
137+ PresentationStrategy presentationStrategy = DEFAULT_PRESENTATION_STRATEGY ;
138+
139+ if (pathVariant ) {
140+ String arg6 = raw .length > 6 ? raw [6 ] : "" ;
141+ String arg7 = raw .length > 7 ? raw [7 ] : "" ;
142+
143+ if (!arg6 .isEmpty ()) {
144+ Double maybePoints = tryParseDouble (arg6 );
145+ if (maybePoints != null ) {
146+ // arg6 is numeric -> points. presentationStrategy, if present, is arg7.
147+ points = maybePoints .intValue ();
148+ if (!arg7 .isEmpty ()) {
149+ presentationStrategy = parsePresentationStrategy (arg7 );
150+ }
151+ } else {
152+ // arg6 isn't numeric -> it's presentationStrategy, and points was omitted.
153+ presentationStrategy = parsePresentationStrategy (arg6 );
154+ if (!arg7 .isEmpty ()) {
155+ throw new IllegalArgumentException (
156+ "Unexpected 8th argument '" + arg7 + "' — the 7th argument ('" + arg6
157+ + "') was already read as the presentation strategy, so no further "
158+ + "trailing arguments are expected. If you meant to supply points, "
159+ + "put it before the presentation strategy." );
160+ }
161+ }
162+ } else if (!arg7 .isEmpty ()) {
163+ // points slot explicitly left empty, but presentationStrategy was still supplied.
164+ presentationStrategy = parsePresentationStrategy (arg7 );
165+ }
114166 }
115167
116-
117- return new DiffEqnCall (kind , rhsText , t0 , y0 , tEnd , h , method , points );
168+ return new DiffEqnCall (kind , rhsText , t0 , y0 , tEnd , h , method , points , presentationStrategy );
118169 }
119170
120171 private static double parseDouble (String raw , String argName ) {
@@ -125,6 +176,20 @@ private static double parseDouble(String raw, String argName) {
125176 }
126177 }
127178
179+ /**
180+ * Like {@link #parseDouble}, but never throws — returns null on failure
181+ * instead. Used to disambiguate whether a trailing optional argument is
182+ * numeric (points) or not (presentationStrategy) without relying on a
183+ * fixed position, since the two are independently optional.
184+ */
185+ private static Double tryParseDouble (String raw ) {
186+ try {
187+ return Double .parseDouble (raw .trim ());
188+ } catch (NumberFormatException e ) {
189+ return null ;
190+ }
191+ }
192+
128193 /**
129194 * Resolves y0 (argument index 2) from its real, isolated {@code Token}
130195 * rather than raw text where possible — see class javadoc for why. A
@@ -202,4 +267,26 @@ public static ODESolverMethod parseMethod(String raw) {
202267 + "\" euler\" , \" rk4\" , \" rk45\" , \" implicit_euler\" , \" bdf2\" ." );
203268 }
204269 }
205- }
270+
271+ /**
272+ * Parses the optional presentationStrategy argument. Accepts an
273+ * unquoted or quoted "state"/"trajectory" (case-insensitive), matching
274+ * {@link #parseMethod}'s convention for string-valued arguments.
275+ */
276+ public static PresentationStrategy parsePresentationStrategy (String raw ) {
277+ String cleaned = raw .trim ();
278+ if (cleaned .length () >= 2 && cleaned .startsWith ("\" " ) && cleaned .endsWith ("\" " )) {
279+ cleaned = cleaned .substring (1 , cleaned .length () - 1 );
280+ }
281+ switch (cleaned .toLowerCase ()) {
282+ case "trajectory" :
283+ return PresentationStrategy .TRAJECTORY ;
284+ case "state" :
285+ return PresentationStrategy .STATE ;
286+ default :
287+ throw new IllegalArgumentException (
288+ "Unrecognized presentation strategy '" + raw + "' — expected one of "
289+ + "\" trajectory\" or \" state\" ." );
290+ }
291+ }
292+ }
0 commit comments