Skip to content

Commit

Permalink
Add rtl option to coerce from right to left
Browse files Browse the repository at this point in the history
Close: #248
  • Loading branch information
isaacs committed Jul 1, 2019
1 parent d062593 commit 388ec1c
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 23 deletions.
39 changes: 26 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ Options:
Coerce a string into SemVer if possible
(does not imply --loose)

--rtl
Coerce version strings right to left

--ltr
Coerce version strings left to right (default)

Program exits successfully if any valid version satisfies
all supplied ranges, and prints all satisfying versions.

Expand Down Expand Up @@ -399,19 +405,26 @@ range, use the `satisfies(version, range)` function.

### Coercion

* `coerce(version)`: Coerces a string to semver if possible

This aims to provide a very forgiving translation of a non-semver
string to semver. It looks for the first digit in a string, and
consumes all remaining characters which satisfy at least a partial semver
(e.g., `1`, `1.2`, `1.2.3`) up to the max permitted length (256 characters).
Longer versions are simply truncated (`4.6.3.9.2-alpha2` becomes `4.6.3`).
All surrounding text is simply ignored (`v3.4 replaces v3.3.1` becomes `3.4.0`).
Only text which lacks digits will fail coercion (`version one` is not valid).
The maximum length for any semver component considered for coercion is 16 characters;
longer components will be ignored (`10000000000000000.4.7.4` becomes `4.7.4`).
The maximum value for any semver component is `Integer.MAX_SAFE_INTEGER || (2**53 - 1)`;
higher value components are invalid (`9999999999999999.4.7.4` is likely invalid).
* `coerce(version, options)`: Coerces a string to semver if possible

This aims to provide a very forgiving translation of a non-semver string to
semver. It looks for the first digit in a string, and consumes all
remaining characters which satisfy at least a partial semver (e.g., `1`,
`1.2`, `1.2.3`) up to the max permitted length (256 characters). Longer
versions are simply truncated (`4.6.3.9.2-alpha2` becomes `4.6.3`). All
surrounding text is simply ignored (`v3.4 replaces v3.3.1` becomes
`3.4.0`). Only text which lacks digits will fail coercion (`version one`
is not valid). The maximum length for any semver component considered for
coercion is 16 characters; longer components will be ignored
(`10000000000000000.4.7.4` becomes `4.7.4`). The maximum value for any
semver component is `Integer.MAX_SAFE_INTEGER || (2**53 - 1)`; higher value
components are invalid (`9999999999999999.4.7.4` is likely invalid).

If the `options.rtl` flag is set, then `coerce` will return the right-most
coercible tuple that does not share an ending index with a longer coercible
tuple. For example, `1.2.3.4` will return `2.3.4` in rtl mode, not
`4.0.0`. `1.2.3/4` will return `4.0.0`, because the `4` is not a part of
any other overlapping SemVer tuple.

### Clean

Expand Down
18 changes: 16 additions & 2 deletions bin/semver
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ var includePrerelease = false

var coerce = false

var rtl = false

var identifier

var semver = require('../semver')
Expand Down Expand Up @@ -71,6 +73,12 @@ function main () {
case '-c': case '--coerce':
coerce = true
break
case '--rtl':
rtl = true
break
case '--ltr':
rtl = false
break
case '-h': case '--help': case '-?':
return help()
default:
Expand All @@ -79,10 +87,10 @@ function main () {
}
}

var options = { loose: loose, includePrerelease: includePrerelease }
var options = { loose: loose, includePrerelease: includePrerelease, rtl: rtl }

versions = versions.map(function (v) {
return coerce ? (semver.coerce(v) || { version: v }).version : v
return coerce ? (semver.coerce(v, options) || { version: v }).version : v
}).filter(function (v) {
return semver.valid(v)
})
Expand Down Expand Up @@ -149,6 +157,12 @@ function help () {
' Coerce a string into SemVer if possible',
' (does not imply --loose)',
'',
'--rtl',
' Coerce version strings right to left',
'',
'--ltr',
' Coerce version strings left to right (default)',
'',
'Program exits successfully if any valid version satisfies',
'all supplied ranges, and prints all satisfying versions.',
'',
Expand Down
40 changes: 34 additions & 6 deletions semver.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,13 @@ src[XRANGELOOSE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAINLOOSE] + '$'
// Coercion.
// Extract anything that could conceivably be a part of a valid semver
var COERCE = R++
src[COERCE] = '(?:^|[^\\d])' +
src[COERCE] = '(^|[^\\d])' +
'(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '})' +
'(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' +
'(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' +
'(?:$|[^\\d])'
var COERCERTL = R++
re[COERCERTL] = new RegExp(src[COERCE], 'g')

// Tilde ranges.
// Meaning is "reasonably at or greater than"
Expand Down Expand Up @@ -1549,13 +1551,39 @@ function coerce (version, options) {
return null
}

var match = version.match(re[COERCE])
options = options || {}

if (match == null) {
var match = null
if (!options.rtl) {
match = version.match(re[COERCE])
} else {
// Find the right-most coercible string that does not share
// a terminus with a more left-ward coercible string.
// Eg, '1.2.3.4' wants to coerce '2.3.4', not '3.4' or '4'
//
// Walk through the string checking with a /g regexp
// Manually set the index so as to pick up overlapping matches.
// Stop when we get a match that ends at the string end, since no
// coercible string can be more right-ward without the same terminus.
var next
while ((next = re[COERCERTL].exec(version)) &&
(!match || match.index + match[0].length !== version.length)
) {
if (!match ||
next.index + next[0].length !== match.index + match[0].length) {
match = next
}
re[COERCERTL].lastIndex = next.index + next[1].length + next[2].length
}
// leave it in a clean state
re[COERCERTL].lastIndex = -1
}

if (match === null) {
return null
}

return parse(match[1] +
'.' + (match[2] || '0') +
'.' + (match[3] || '0'), options)
return parse(match[2] +
'.' + (match[3] || '0') +
'.' + (match[4] || '0'), options)
}
14 changes: 12 additions & 2 deletions test/coerce.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,21 @@ test('\ncoerce tests', function (t) {
['1.2.3.' + r('4')(1024), '1.2.3'],
[r('1')(17) + '.4.7.4', '4.7.4'],
[10, '10.0.0'],
].forEach(function (tuple) {
['1.2.3/a/b/c/2.3.4', '2.3.4', { rtl: true }],
['1.2.3.4.5.6', '4.5.6', { rtl: true }],
['1.2.3.4.5/6', '6.0.0', { rtl: true }],
['1.2.3.4./6', '6.0.0', { rtl: true }],
['1.2.3.4/6', '6.0.0', { rtl: true }],
['1.2.3./6', '6.0.0', { rtl: true }],
['1.2.3/6', '6.0.0', { rtl: true }],
['1.2.3.4', '2.3.4', { rtl: true }],
['1.2.3.4xyz', '2.3.4', { rtl: true }],
].forEach(function (tuple, i) {
var input = tuple[0]
var expected = tuple[1]
var options = tuple[2]
var msg = 'coerce(' + input + ') should become ' + expected
t.same((coerce(input) || {}).version, expected, msg)
t.same((coerce(input, options) || {}).version, expected, msg)
})

t.same(valid(coerce('42.6.7.9.3-alpha')), '42.6.7')
Expand Down

0 comments on commit 388ec1c

Please sign in to comment.