A quick shot, free-spacing, likely far from efficient (first of all because a single range will make the regex backtrack when it finds out there's not a '|' after it) - see version 2 below, which is (I believe) much more efficient:
^ # Start of string
(?: # Start group, non-capturing
[([] # '(' or '['
\s* # optional whitespace
[0-9]+ # at least one digit 0-9
(?:\.[0-9]+)? # optionally '.' followed by at least one digit 0-9
\s* # optional whitespace
, # ','
\s* # optional whitespace
[0-9]+ # at least one digit 0-9
(?:\.[0-9]+)? # optionally '.' followed by at least one digit 0-9
\s* # optional whitespace
[)\]] # ')' or ']'
\s* # optional whitespace
\| # '|'
\s* # optional whitespace
)* # all the above may appear 0 or more times
[([] # The remainder is exactly the same as the group above,
\s* # used for a single range or the last range -
[0-9]+ # i.e., a range NOT followed by '|' - of a multi range.
(?:\.[0-9]+)?
\s*
,
\s*
[0-9]+
(?:\.[0-9]+)?
\s*
[)\]]
$ # end of string
This will match e.g.:
[1.5, 3]
[23.7, 3.70)
[2.9 , 3]|[3,2)
[1.5, 4] | [6.9, 9.3) | [10, 11]
(1.5, 3]
[23.7, 3.70)
(1.5, 5.0)
But not:
[23.7, 3.70) | (7, 9) | // trailing OR
| [23.7, 3.7] // leading OR
Note that it doesn't ensure that the second number is actually higher than the first. For that, I really recommend leaving it to the/a parser - or add capturing groups and process them outside regex.
VERSION 2
This should be more efficient due to less backtracking - it's basically changed from:
(any number of ranges followed by |) followed by a range
... to:
a range followed by (any number of ranges preceded by |)
ETA: To explain, version 1 starts out checking for "a range followed by |".
If we only have a single range, that's wasted time. When it gets to the "|" it will start over, checking the second part of the regex - i.e. is there, instead, the required "range without |"?
In version 2, instead, we start out checking for simply "a range". That means, if there's only one range, it will succeed without any backtracking. If we give it gibberish, e.g. hello
, it will fail immediately, because it now knows that the first character must be a (
or [
- it's not optional. Whereas in version 1, because the first part was optional, it would then have to check the second part of the regex to make sure that failed too.
In just about every other case (that I've tested) version 2 matches - or fails to match - in fewer steps.
Here, since it's basically the same regex with some parts switched, I'll instead put an example match in the comments:
^
[([] # (
\s* #
[0-9]+ # 3
(?:\.[0-9]+)? # .90
\s* #
, # ,
\s* #
[0-9]+ # 43
(?:\.[0-9]+)? # .2
\s* #
[)\]] # ]
#
(?: #
\s* #
\| # |
\s* #
#
[([] # [
\s* #
[0-9]+ # 55
(?:\.[0-9]+)? # .20
\s* #
, # ,
\s* #
[0-9]+ # 2
(?:\.[0-9]+)? # .91
\s* #
[)\]] # )
)*
$
Matches and non-matches should be identical to version 1.