I'm looking for a "compact" expression that will return the
length of the longest contiguous block of (numeric, base 10)
digits in a string. Something in the form of:
var mystr = "xy97 z0326 0654 943ab"
alert (... mystr ...) => 4
Further examples:
"" and "fred" => 0
"234" => 3
"10/23/2009" => 4
"12 345678901234567 89" => 15
"I usually awake after 9:30" => 2
"372 Myrtle Lane" => 3
"New York, NY 10001-2345" => 5
Note: If your solution involves a loop, then you should wrap
it in a function. Can you write it without any explicit loops?
To clarify, the intent of the exercise was not "cute" solutions.
Rather, my interest was on array transformations as a way of
looking at these types of problems (a value needed from an
array of data) rather than loops. Loops are still implied,
but javascript internal loops ought to be far more efficient
than explicit for loops. The reason for the alert(...mystr...)
form was to encourage the single expression solution.
There have been a lot of good ideas on this exercise. One thing
that might make things easier is the realization that the values
of the digits are immaterial. In particular, by doing
s.replace(/\d/,"1") the problem is reduced to that of finding
the longest sequence of 1s. That makes a difference because
now we don't have to worry about pesky leading 0s and sorting
lexigraphically is now equivalent to sorting numerically.
All four of the following examples assume that the source
string is s.
For example, we can now do:
alert((s.replace(/\d/g,"1") // convert nums to all 1s
.split(/\D+/) // make array of the 1 strings
.sort() // sort them (longest at end)
.slice(-1)[0]||'') // get the final one
.length); // and show its length
Using Math.max is not so smooth because the degenerate
cases on FF/IE are treated differently by Math.max
The problem can be circumvented by augmenting each
string by 1, and then subtracting 1 off of the length
at the end. Note that the "1 " string being prepended
serves two purposes. The 1 ensures that match will find
at least one entry, and the space ensures that the first
numeric sequence has a non one prefixing it so that it
will be augemented by 1 in the next step.
alert((Math.max.apply(0, // Take a max
('1 ' // Corresponds to 0
+ s.replace(/\d/g,"1")) // Convert each digit to 1
.replace(/\D1/g," 11") // Augment each # (except 1st) by "1"
.match(/\d+/g) // Make an array of the 1 strings
)+'') // Coerce max to string
.length-1); // Find augmented len, subtract 1
For Vikram's pleasure, here are two RegExp approaches.
This first one works by removing a digit from each
contiguous block of digits on each iteration of a loop.
The length of the longest contiguous block is the number
of iterations through the loop until all the digits are gone.
for (var c=0; // counter c
s.match(/\d/); // Loop until no more digits in s
s=s.replace( // Remove a 1 from each block
/\d(?=\D|$)/g)) // of digits
++c; // Count the number of iterations
alert(c);
This final method is a "loopless" regular expression, but
internally it's quite busy. I was surprised to have it
come out so compactly. As with all my examples, the digits
are first replaced with a 1. The match is divided into two
parts. The shorter left half accounts for those cases
where there is no digit in the original string.
The right helf of the match is a bit trickier, and I will
gloss over some of the details. The (1+) says to identify
a complete contiguous block of 1s. It's complete because
the 1+ is greedy and slurps up as many 1s as it can (If
the rest of the pattern fails, then it will also fail if
less 1s are slurped up - I think PHP regular expressions
have a way of preventing the backtracking, but I don't
think javascript regular expressions support it).
In any case, this contiguous block of 1s is remembered
(captured). Now here's the important point: If this
is the longest contiguous block, then this captured
string must not appear at any other point in the string.
In other words, our match will be successful, if our
captured string does not match anywhere in the rest
of the string! In other words, we want to do a
negative lookahead at every remaining character in the
string, and that is exactly what the rest of the
right half is doing. Now should the captured group
match (which means that the overall pattern fails at
that point), that means there is a group of 1s to the
right of the current point which are at least as long
and so the pattern will eventually migrate the
captured block of 1s to that point and try from there.
The [1] accesses the captured (longest) block.
Under FF, this is undefined when there is no capture
(which happens when there are no digits in the
original string, so for that reason there is a ||""
along with surrounding parens. Finally, the length
of the captured (longest) block is returned.
Here's the expression (comments in the regular
expression must be removed before running):
alert((
s.replace(/\d/g,"1") // Convert all digits to 1
.match(/^\D*$| // Match if no 1s
(1+) // Capture block of 1s
(.(?!\1))*$/) // Match if captured block
// is not repeated
[1]||"") // Access captured block
.length); // and get its length
Csaba Gabor from Vienna