Skip to content

Commit 388ec1c

Browse files
committed
Add rtl option to coerce from right to left
Close: #248
1 parent d062593 commit 388ec1c

File tree

4 files changed

+88
-23
lines changed

4 files changed

+88
-23
lines changed

README.md

+26-13
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ Options:
6060
Coerce a string into SemVer if possible
6161
(does not imply --loose)
6262

63+
--rtl
64+
Coerce version strings right to left
65+
66+
--ltr
67+
Coerce version strings left to right (default)
68+
6369
Program exits successfully if any valid version satisfies
6470
all supplied ranges, and prints all satisfying versions.
6571

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

400406
### Coercion
401407

402-
* `coerce(version)`: Coerces a string to semver if possible
403-
404-
This aims to provide a very forgiving translation of a non-semver
405-
string to semver. It looks for the first digit in a string, and
406-
consumes all remaining characters which satisfy at least a partial semver
407-
(e.g., `1`, `1.2`, `1.2.3`) up to the max permitted length (256 characters).
408-
Longer versions are simply truncated (`4.6.3.9.2-alpha2` becomes `4.6.3`).
409-
All surrounding text is simply ignored (`v3.4 replaces v3.3.1` becomes `3.4.0`).
410-
Only text which lacks digits will fail coercion (`version one` is not valid).
411-
The maximum length for any semver component considered for coercion is 16 characters;
412-
longer components will be ignored (`10000000000000000.4.7.4` becomes `4.7.4`).
413-
The maximum value for any semver component is `Integer.MAX_SAFE_INTEGER || (2**53 - 1)`;
414-
higher value components are invalid (`9999999999999999.4.7.4` is likely invalid).
408+
* `coerce(version, options)`: Coerces a string to semver if possible
409+
410+
This aims to provide a very forgiving translation of a non-semver string to
411+
semver. It looks for the first digit in a string, and consumes all
412+
remaining characters which satisfy at least a partial semver (e.g., `1`,
413+
`1.2`, `1.2.3`) up to the max permitted length (256 characters). Longer
414+
versions are simply truncated (`4.6.3.9.2-alpha2` becomes `4.6.3`). All
415+
surrounding text is simply ignored (`v3.4 replaces v3.3.1` becomes
416+
`3.4.0`). Only text which lacks digits will fail coercion (`version one`
417+
is not valid). The maximum length for any semver component considered for
418+
coercion is 16 characters; longer components will be ignored
419+
(`10000000000000000.4.7.4` becomes `4.7.4`). The maximum value for any
420+
semver component is `Integer.MAX_SAFE_INTEGER || (2**53 - 1)`; higher value
421+
components are invalid (`9999999999999999.4.7.4` is likely invalid).
422+
423+
If the `options.rtl` flag is set, then `coerce` will return the right-most
424+
coercible tuple that does not share an ending index with a longer coercible
425+
tuple. For example, `1.2.3.4` will return `2.3.4` in rtl mode, not
426+
`4.0.0`. `1.2.3/4` will return `4.0.0`, because the `4` is not a part of
427+
any other overlapping SemVer tuple.
415428

416429
### Clean
417430

bin/semver

+16-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ var includePrerelease = false
1919

2020
var coerce = false
2121

22+
var rtl = false
23+
2224
var identifier
2325

2426
var semver = require('../semver')
@@ -71,6 +73,12 @@ function main () {
7173
case '-c': case '--coerce':
7274
coerce = true
7375
break
76+
case '--rtl':
77+
rtl = true
78+
break
79+
case '--ltr':
80+
rtl = false
81+
break
7482
case '-h': case '--help': case '-?':
7583
return help()
7684
default:
@@ -79,10 +87,10 @@ function main () {
7987
}
8088
}
8189

82-
var options = { loose: loose, includePrerelease: includePrerelease }
90+
var options = { loose: loose, includePrerelease: includePrerelease, rtl: rtl }
8391

8492
versions = versions.map(function (v) {
85-
return coerce ? (semver.coerce(v) || { version: v }).version : v
93+
return coerce ? (semver.coerce(v, options) || { version: v }).version : v
8694
}).filter(function (v) {
8795
return semver.valid(v)
8896
})
@@ -149,6 +157,12 @@ function help () {
149157
' Coerce a string into SemVer if possible',
150158
' (does not imply --loose)',
151159
'',
160+
'--rtl',
161+
' Coerce version strings right to left',
162+
'',
163+
'--ltr',
164+
' Coerce version strings left to right (default)',
165+
'',
152166
'Program exits successfully if any valid version satisfies',
153167
'all supplied ranges, and prints all satisfying versions.',
154168
'',

semver.js

+34-6
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,13 @@ src[XRANGELOOSE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAINLOOSE] + '$'
160160
// Coercion.
161161
// Extract anything that could conceivably be a part of a valid semver
162162
var COERCE = R++
163-
src[COERCE] = '(?:^|[^\\d])' +
163+
src[COERCE] = '(^|[^\\d])' +
164164
'(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '})' +
165165
'(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' +
166166
'(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' +
167167
'(?:$|[^\\d])'
168+
var COERCERTL = R++
169+
re[COERCERTL] = new RegExp(src[COERCE], 'g')
168170

169171
// Tilde ranges.
170172
// Meaning is "reasonably at or greater than"
@@ -1549,13 +1551,39 @@ function coerce (version, options) {
15491551
return null
15501552
}
15511553

1552-
var match = version.match(re[COERCE])
1554+
options = options || {}
15531555

1554-
if (match == null) {
1556+
var match = null
1557+
if (!options.rtl) {
1558+
match = version.match(re[COERCE])
1559+
} else {
1560+
// Find the right-most coercible string that does not share
1561+
// a terminus with a more left-ward coercible string.
1562+
// Eg, '1.2.3.4' wants to coerce '2.3.4', not '3.4' or '4'
1563+
//
1564+
// Walk through the string checking with a /g regexp
1565+
// Manually set the index so as to pick up overlapping matches.
1566+
// Stop when we get a match that ends at the string end, since no
1567+
// coercible string can be more right-ward without the same terminus.
1568+
var next
1569+
while ((next = re[COERCERTL].exec(version)) &&
1570+
(!match || match.index + match[0].length !== version.length)
1571+
) {
1572+
if (!match ||
1573+
next.index + next[0].length !== match.index + match[0].length) {
1574+
match = next
1575+
}
1576+
re[COERCERTL].lastIndex = next.index + next[1].length + next[2].length
1577+
}
1578+
// leave it in a clean state
1579+
re[COERCERTL].lastIndex = -1
1580+
}
1581+
1582+
if (match === null) {
15551583
return null
15561584
}
15571585

1558-
return parse(match[1] +
1559-
'.' + (match[2] || '0') +
1560-
'.' + (match[3] || '0'), options)
1586+
return parse(match[2] +
1587+
'.' + (match[3] || '0') +
1588+
'.' + (match[4] || '0'), options)
15611589
}

test/coerce.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,21 @@ test('\ncoerce tests', function (t) {
105105
['1.2.3.' + r('4')(1024), '1.2.3'],
106106
[r('1')(17) + '.4.7.4', '4.7.4'],
107107
[10, '10.0.0'],
108-
].forEach(function (tuple) {
108+
['1.2.3/a/b/c/2.3.4', '2.3.4', { rtl: true }],
109+
['1.2.3.4.5.6', '4.5.6', { rtl: true }],
110+
['1.2.3.4.5/6', '6.0.0', { rtl: true }],
111+
['1.2.3.4./6', '6.0.0', { rtl: true }],
112+
['1.2.3.4/6', '6.0.0', { rtl: true }],
113+
['1.2.3./6', '6.0.0', { rtl: true }],
114+
['1.2.3/6', '6.0.0', { rtl: true }],
115+
['1.2.3.4', '2.3.4', { rtl: true }],
116+
['1.2.3.4xyz', '2.3.4', { rtl: true }],
117+
].forEach(function (tuple, i) {
109118
var input = tuple[0]
110119
var expected = tuple[1]
120+
var options = tuple[2]
111121
var msg = 'coerce(' + input + ') should become ' + expected
112-
t.same((coerce(input) || {}).version, expected, msg)
122+
t.same((coerce(input, options) || {}).version, expected, msg)
113123
})
114124

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

0 commit comments

Comments
 (0)