Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5a27c11
feat: give unary % operator (percentage) higher precedence than binar…
kiprobinsonknack Apr 16, 2025
4f748d2
feat: give unary % operator (percentage) higher precedence than binar…
kiprobinsonknack Apr 16, 2025
75cd91b
chore: update HISTORY, AUTHORS, and fix doc typo from last commit (#3…
gwhitney Apr 22, 2025
72a2442
Feat: Enhance Kronecker product to handle arbitrary dimension (#3461)
gwhitney Apr 23, 2025
87651fe
Merge remote-tracking branch 'origin/v15' into v15
josdejong May 7, 2025
8e76654
Merge branch 'develop' into v15
josdejong Jun 4, 2025
9aad1a0
feat: give unary % operator (percentage) higher precedence than binar…
kiprobinsonknack Apr 16, 2025
e8caa78
chore: update HISTORY, AUTHORS, and fix doc typo from last commit (#3…
gwhitney Apr 22, 2025
5655d71
Feat: Enhance Kronecker product to handle arbitrary dimension (#3461)
gwhitney Apr 23, 2025
b44172b
fix: #3501 parse % as unary only when not followed by a term (#3505)
gwhitney Jul 4, 2025
ba88f47
fix: #3421 require a space or delimiter after hex, bin, and oct value…
josdejong Jul 11, 2025
311485f
Merge remote-tracking branch 'origin/v15' into v15
josdejong Jul 11, 2025
e06e89f
chore: update HISTORY and AUTHORS
josdejong Jul 11, 2025
6b3b722
chore: update unit test
josdejong Jul 11, 2025
10602fd
feat: matrix subset according to type of input (#3485)
dvd101x Jul 16, 2025
7dda1d7
docs: update HISTORY.md and fix some duplicate lines
josdejong Jul 16, 2025
a8ec24d
docs: some updates in the description to migrate matrix index behavio…
josdejong Jul 16, 2025
1e3c2a9
Merge branch 'develop' into v15
josdejong Sep 24, 2025
f894645
chore: update unit tests
josdejong Sep 24, 2025
f74d636
Merge branch 'develop' into v15
josdejong Sep 26, 2025
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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ witer33 <[email protected]>
Hudsxn <[email protected]>
Christian Stussak <[email protected]>
aitee <[email protected]>
Kip Robinson <[email protected]>
Delaney Sylvans <[email protected]>
Rani D. <[email protected]>
Don McCurdy <[email protected]>
Expand Down
17 changes: 17 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# History

# Unpublished changes scheduled for v15

!!! BE CAREFUL: BREAKING CHANGES !!!

- Fix: #1753 Correct dimensionality of Kronecker product on vectors (and
extend to arbitrary dimension) (#3455). Thanks @Delaney.
- Fix: #3501 parse `%` as unary only when not followed by a term (#3505).
Thanks @gwhitney.
- Fix #3421 require a space or delimiter after hex, bin, and oct values (#3463).
- Feat: #3349 Decouple precedence of unary percentage operator and binary
modulus operator (that both use symbol `%`), and raise the former (#3432).
Thanks @kiprobinsonknack.
- Feat: #1753 enhance Kronecker product to handle arbitrary dimension (#3461).
Thanks @gwhitney.
- Feat: #2344 matrix subset according to the type of input (#3485).
Thanks @dvd101x.

# 2025-09-26, 14.8.1

- Fix: #3538 `config` printing a warning when using `{ number: 'bigint' }`
Expand Down
5 changes: 4 additions & 1 deletion docs/core/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const config = {
numberFallback: 'number',
precision: 64,
predictable: false,
randomSeed: null
randomSeed: null,
legacySubset: false
}
const math = create(all, config)

Expand Down Expand Up @@ -84,6 +85,8 @@ The following configuration options are available:

- `randomSeed`. Set this option to seed pseudo random number generation, making it deterministic. The pseudo random number generator is reset with the seed provided each time this option is set. For example, setting it to `'a'` will cause `math.random()` to return `0.43449421599986604` upon the first call after setting the option every time. Set to `null` to seed the pseudo random number generator with a random seed. Default value is `null`.

- `legacySubset`. When set to `true`, the `subset` function behaves as in earlier versions of math.js: retrieving a subset where the index size contains only one element, returns the value itself. When set to `false` (default), `subset` eliminates dimensions from the result only if the index dimension is a scalar; if the index dimension is a range, matrix, or array, the dimensions are preserved. This option is helpful for maintaining compatibility with legacy code that depends on the previous behavior.


## Examples

Expand Down
61 changes: 55 additions & 6 deletions docs/datatypes/matrices.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,34 @@ in the matrix, and if not, a subset of the matrix will be returned.

A subset can be defined using an `Index`. An `Index` contains a single value
or a set of values for each dimension of a matrix. An `Index` can be
created using the function `index`. When getting a single value from a matrix,
`subset` will return the value itself instead of a matrix containing just this
value.
created using the function `index`. The way `subset` returns results depends on how you specify indices for each dimension:

The function `subset` normally returns a subset, but when getting or setting a
single value in a matrix, the value itself is returned.
- If you use a scalar (single number) as an index for a dimension, that dimension is removed from the result.
- If you use an array, matrix or range (even with just one element) as an index, that dimension is preserved in the result.

This means that scalar indices eliminate dimensions, while array, matrix or range indices retain them. See the section [Migrate to v15](#migrate-to-v15) for more details and examples of this behavior.

For example:

```js
const m = [
[10, 11, 12],
[20, 21, 22]
]

// Scalar index eliminates the dimension:
math.subset(m, math.index(1, 2)) // 22 (both dimensions indexed by scalars, result is a value)
math.subset(m, math.index(1, [2])) // [22] (row dimension eliminated, column dimension preserved as array)
math.subset(m, math.index([1], 2)) // [22] (column dimension eliminated, row dimension preserved as array)
math.subset(m, math.index([1], [2])) // [[22]] (both dimensions preserved as arrays)

math.config({legacySubset: true}) // switch to legacy behavior
math.subset(m, math.index(1, 2)) // 22
math.subset(m, math.index(1, [2])) // 22
math.subset(m, math.index([1], 2)) // 22
math.subset(m, math.index([1], [2])) // 22

```

Matrix indexes in math.js are zero-based, like most programming languages
including JavaScript itself. Note that mathematical applications like Matlab
Expand All @@ -296,7 +317,7 @@ math.subset(a, math.index([2, 3])) // Array, [2, 3]
math.subset(a, math.index(math.range(0,4))) // Array, [0, 1, 2, 3]
math.subset(b, math.index(1, 0)) // 2
math.subset(b, math.index(1, [0, 1])) // Array, [2, 3]
math.subset(b, math.index([0, 1], 0)) // Matrix, [[0], [2]]
math.subset(b, math.index([0, 1], [0])) // Matrix, [[0], [2]]

// get a subset
d.subset(math.index([1, 2], [0, 1])) // Matrix, [[3, 4], [6, 7]]
Expand All @@ -313,6 +334,34 @@ e.resize([2, 3], 0) // Matrix, [[0, 0, 0], [0, 0, 0]]
e.subset(math.index(1, 2), 5) // Matrix, [[0, 0, 0], [0, 0, 5]]
```

## Migrate indexing behavior to mathjs v15

With the release of math.js v15, the behavior of `subset` when indexing matrices and arrays has changed. If your code relies on the previous behavior (where indexing with an array or matrix of size 1 would always return the value itself), you may need to update your code or enable legacy mode.

To maintain the old indexing behavior without need for any code changes, use the configuration option `legacySubset`:

```js
math.config({ legacySubset: true })
```

To migrate your code, you'll have to change all matrix indexes from the old index notation to the new index notation. Basically: scalar indexes have to be wrapped in array brackets if you want an array as output. Here some examples:

```js
const m = math.matrix([[1, 2, 3], [4, 5, 6]])
```

| v14 code | v15 equivalent code | Result |
|----------------------------------------------|-------------------------------------------|--------------------|
| `math.subset(m, math.index([0, 1], [1, 2]))` | No change needed | `[[2, 3], [5, 6]]` |
| `math.subset(m, math.index(1, [1, 2]))` | `math.subset(m, math.index([1], [1, 2]))` | `[[5, 6]]` |
| `math.subset(m, math.index([0, 1], 2))` | `math.subset(m, math.index([0, 1], [2]))` | `[[3], [6]]` |
| `math.subset(m, math.index(1, 2))` | No change needed | 6 |


> **Tip:**
> If you want to get a scalar value, use scalar indices.
> If you want to preserve dimensions, use array, matrix or range indices.

## Getting and setting a value in a matrix

There are two methods available on matrices that allow to get or set a single
Expand Down
42 changes: 37 additions & 5 deletions docs/expressions/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ Operators | Description
`??` | Nullish coalescing
`^`, `.^` | Exponentiation
`+`, `-`, `~`, `not` | Unary plus, unary minus, bitwise not, logical not
`%` | Unary percentage
See section below | Implicit multiplication
`*`, `/`, `.*`, `./`,`%`, `mod` | Multiply, divide , percentage, modulus
`*`, `/`, `.*`, `./`,`%`, `mod` | Multiply, divide, modulus
`+`, `-` | Add, subtract
`:` | Range
`to`, `in` | Unit conversion
Expand All @@ -138,8 +139,8 @@ See section below | Implicit multiplication
`\n`, `;` | Statement separators

Lazy evaluation is used where logically possible for bitwise and logical
operators. In the following example, the value of `x` will not even be
evaluated because it cannot effect the final result:
operators. In the following example, the sub-expression `x` will not even be
evaluated because it cannot affect the final result:
```js
math.evaluate('false and x') // false, no matter what x equals
```
Expand Down Expand Up @@ -677,13 +678,15 @@ at 1, when the end is undefined, the range will end at the end of the matrix.

There is a context variable `end` available as well to denote the end of the
matrix. This variable cannot be used in multiple nested indices. In that case,
`end` will be resolved as the end of the innermost matrix. To solve this,
`end` will be resolved as the end of the innermost matrix. To solve this,
resolving of the nested index needs to be split in two separate operations.

*IMPORTANT: matrix indexes and ranges work differently from the math.js indexes
in JavaScript: They are one-based with an included upper-bound, similar to most
math applications.*

*IMPORTANT: The matrix indexing behavior has been changed in mathjs `v15`, see section [Migrate matrix indexing to mathjs v15](#migrate-matrix-indexing-to-mathjs-v15).*

```js
parser = math.parser()

Expand All @@ -701,10 +704,39 @@ parser.evaluate('d = a * b') // Matrix, [[19, 22], [43, 50]]

// retrieve a subset of a matrix
parser.evaluate('d[2, 1]') // 43
parser.evaluate('d[2, 1:end]') // Matrix, [[43, 50]]
parser.evaluate('d[2, 1:end]') // Matrix, [43, 50]
parser.evaluate('c[end - 1 : -1 : 2]') // Matrix, [8, 7, 6]
```

#### Migrate matrix indexing to mathjs v15

With mathjs v15, matrix indexing has changed to be more consistent and predictable. In v14, using a scalar index would sometimes reduce the dimensionality of the result. In v15, if you want to preserve dimensions, use array, matrix, or range indices. If you want a scalar value, use scalar indices.

To maintain the old indexing behavior without need for any code changes, use the configuration option `legacySubset`:

```js
math.config({ legacySubset: true })
```

To migrate your code, you'll have to change all matrix indexes from the old index notation to the new index notation. Basically: scalar indexes have to be wrapped in array brackets if you want an array as output. Here some examples:

```js
parser = math.parser()
parser.evaluate('m = [1, 2, 3; 4, 5, 6]')
```

| v14 code | v15 equivalent code | Result |
|-------------------------|-------------------------------|--------------------|
| `m[1:2, 2:3]` | No change needed | `[[2, 3], [5, 6]]` |
| `m[2, 2:3]` | `m[[2], 2:3]` | `[[5, 6]]` |
| `m[1:2, 3]` | `m[1:2, [3]]` | `[[3], [6]]` |
| `m[2, 3]` | No change needed | `6` |

> **Tip:**
> If you want to always get a scalar value, use scalar indices.
> If you want to preserve dimensions, use array, matrix or range indices.


## Objects

Objects in math.js work the same as in languages like JavaScript and Python.
Expand Down
7 changes: 6 additions & 1 deletion src/core/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,10 @@ export const DEFAULT_CONFIG = {

// random seed for seeded pseudo random number generation
// null = randomly seed
randomSeed: null
randomSeed: null,

// legacy behavior for matrix subset. When true, the subset function
// returns a matrix or array with the same size as the index (except for scalars).
// When false, it returns a matrix or array with a size depending on the type of index.
legacySubset: false
}
5 changes: 5 additions & 0 deletions src/core/function/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export function configFactory (config, emit) {
delete optionsFix.epsilon
return _config(optionsFix)
}

if (options.legacySubset === true) {
// this if is only for backwards compatibility, it can be removed in the future.
console.warn('Warning: The configuration option "legacySubset" is for compatibility only and might be deprecated in the future.')
}
const prev = clone(config)

// validate some of the options
Expand Down
83 changes: 46 additions & 37 deletions src/expression/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,10 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
next(state)
state.token += currentCharacter(state)
next(state)
while (parse.isHexDigit(currentCharacter(state))) {
while (
parse.isAlpha(currentCharacter(state), prevCharacter(state), nextCharacter(state)) ||
parse.isDigit(currentCharacter(state))
) {
state.token += currentCharacter(state)
next(state)
}
Expand All @@ -357,7 +360,10 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
state.token += '.'
next(state)
// get the digits after the radix
while (parse.isHexDigit(currentCharacter(state))) {
while (
parse.isAlpha(currentCharacter(state), prevCharacter(state), nextCharacter(state)) ||
parse.isDigit(currentCharacter(state))
) {
state.token += currentCharacter(state)
next(state)
}
Expand Down Expand Up @@ -576,17 +582,6 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
return (c >= '0' && c <= '9')
}

/**
* checks if the given char c is a hex digit
* @param {string} c a string with one character
* @return {boolean}
*/
parse.isHexDigit = function isHexDigit (c) {
return ((c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F'))
}

/**
* Start of the parse levels below, in order of precedence
* @return {Node} node
Expand Down Expand Up @@ -1003,7 +998,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
function parseAddSubtract (state) {
let node, name, fn, params

node = parseMultiplyDivideModulusPercentage(state)
node = parseMultiplyDivideModulus(state)

const operators = {
'+': 'add',
Expand All @@ -1014,7 +1009,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
fn = operators[name]

getTokenSkipNewline(state)
const rightNode = parseMultiplyDivideModulusPercentage(state)
const rightNode = parseMultiplyDivideModulus(state)
if (rightNode.isPercentage) {
params = [node, new OperatorNode('*', 'multiply', [node, rightNode])]
} else {
Expand All @@ -1027,11 +1022,11 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
}

/**
* multiply, divide, modulus, percentage
* multiply, divide, modulus
* @return {Node} node
* @private
*/
function parseMultiplyDivideModulusPercentage (state) {
function parseMultiplyDivideModulus (state) {
let node, last, name, fn

node = parseImplicitMultiplication(state)
Expand All @@ -1051,25 +1046,9 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
// explicit operators
name = state.token
fn = operators[name]

getTokenSkipNewline(state)

if (name === '%' && state.tokenType === TOKENTYPE.DELIMITER && state.token !== '(') {
// If the expression contains only %, then treat that as /100
if (state.token !== '' && operators[state.token]) {
const left = new OperatorNode('/', 'divide', [node, new ConstantNode(100)], false, true)
name = state.token
fn = operators[name]
getTokenSkipNewline(state)
last = parseImplicitMultiplication(state)

node = new OperatorNode(name, fn, [left, last])
} else { node = new OperatorNode('/', 'divide', [node, new ConstantNode(100)], false, true) }
// return node
} else {
last = parseImplicitMultiplication(state)
node = new OperatorNode(name, fn, [node, last])
}
last = parseImplicitMultiplication(state)
node = new OperatorNode(name, fn, [node, last])
} else {
break
}
Expand Down Expand Up @@ -1122,7 +1101,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
* @private
*/
function parseRule2 (state) {
let node = parseUnary(state)
let node = parseUnaryPercentage(state)
let last = node
const tokenStates = []

Expand All @@ -1145,7 +1124,7 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
// Rewind once and build the "number / number" node; the symbol will be consumed later
Object.assign(state, tokenStates.pop())
tokenStates.pop()
last = parseUnary(state)
last = parseUnaryPercentage(state)
node = new OperatorNode('/', 'divide', [node, last])
} else {
// Not a match, so rewind
Expand All @@ -1166,6 +1145,36 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
return node
}

/**
* Unary percentage operator (treated as `value / 100`)
* @return {Node} node
* @private
*/
function parseUnaryPercentage (state) {
let node = parseUnary(state)

if (state.token === '%') {
const previousState = Object.assign({}, state)
getTokenSkipNewline(state)
// We need to decide if this is a unary percentage % or binary modulo %
// So we attempt to parse a unary expression at this point.
// If it fails, then the only possibility is that this is a unary percentage.
// If it succeeds, then we presume that this must be binary modulo, since the
// only things that parseUnary can handle are _higher_ precedence than unary %.
try {
parseUnary(state)
// Not sure if we could somehow use the result of that parseUnary? Without
// further analysis/testing, safer just to discard and let the parse proceed
Object.assign(state, previousState)
} catch {
// Not seeing a term at this point, so was a unary %
node = new OperatorNode('/', 'divide', [node, new ConstantNode(100)], false, true)
}
}

return node
}

/**
* Unary plus and minus, and logical and bitwise not
* @return {Node} node
Expand Down
Loading