Skip to content
This repository was archived by the owner on Jun 9, 2025. It is now read-only.

Commit fe6c205

Browse files
BartoszKolankoKarimGeiger
authored andcommitted
enhancement - add 'not' negated character classes (#40)
*add negated versions of methods generating ranges: -notDigit, -notLetter, -notUppercateLetter -notOneOf
1 parent 96c2873 commit fe6c205

File tree

5 files changed

+125
-5
lines changed

5 files changed

+125
-5
lines changed

src/Builder.php

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,28 @@ public function oneOf(string $chars)
165165
{
166166
$this->validateAndAddMethodType(self::METHOD_TYPE_CHARACTER, self::METHOD_TYPES_ALLOWED_FOR_CHARACTERS);
167167

168-
$chars = implode('', array_map([$this, 'escape'], str_split($chars)));
169-
$chars = str_replace(['-', ']'], ['\\-', '\\]'], $chars);
168+
$chars = $this->escape($chars);
169+
$chars = $this->escapeRangeSpecificChars($chars);
170170

171171
return $this->add('[' . $chars . ']');
172172
}
173173

174+
/**
175+
* Literally match anything but one of these characters.
176+
*
177+
* @param string $chars
178+
* @return Builder
179+
*/
180+
public function notOneOf(string $chars)
181+
{
182+
$this->validateAndAddMethodType(self::METHOD_TYPE_CHARACTER, self::METHOD_TYPES_ALLOWED_FOR_CHARACTERS);
183+
184+
$chars = $this->escape($chars);
185+
$chars = $this->escapeRangeSpecificChars($chars);
186+
187+
return $this->add('[^' . $chars . ']');
188+
}
189+
174190
/**
175191
* Literally match all of these characters in that order.
176192
*
@@ -181,7 +197,7 @@ public function literally(string $chars) : self
181197
{
182198
$this->validateAndAddMethodType(self::METHOD_TYPE_CHARACTER, self::METHOD_TYPES_ALLOWED_FOR_CHARACTERS);
183199

184-
return $this->add('(?:' . implode('', array_map([$this, 'escape'], str_split($chars))) . ')');
200+
return $this->add('(?:' . $this->escape($chars) . ')');
185201
}
186202

187203
/**
@@ -212,6 +228,20 @@ public function digit(int $min = 0, int $max = 9) : self
212228
return $this->add("[$min-$max]");
213229
}
214230

231+
/**
232+
* Match anything but digit (in given span). Default will be a digit between 0 and 9.
233+
*
234+
* @param int $min
235+
* @param int $max
236+
* @return Builder
237+
*/
238+
public function notDigit(int $min = 0, int $max = 9) : self
239+
{
240+
$this->validateAndAddMethodType(self::METHOD_TYPE_CHARACTER, self::METHOD_TYPES_ALLOWED_FOR_CHARACTERS);
241+
242+
return $this->add("[^$min-$max]");
243+
}
244+
215245
/**
216246
* Match any uppercase letter (between A to Z).
217247
*
@@ -238,6 +268,32 @@ public function letter(string $min = 'a', string $max = 'z') : self
238268
return $this->add("[$min-$max]");
239269
}
240270

271+
/**
272+
* Match anything but uppercase letter (between A to Z).
273+
*
274+
* @param string $min
275+
* @param string $max
276+
* @return Builder
277+
*/
278+
public function notUppercaseLetter(string $min = 'A', string $max = 'Z') : self
279+
{
280+
return $this->notLetter($min, $max);
281+
}
282+
283+
/**
284+
* Match anything but lowercase letter (between a to z).
285+
*
286+
* @param string $min
287+
* @param string $max
288+
* @return Builder
289+
*/
290+
public function notLetter(string $min = 'a', string $max = 'z') : self
291+
{
292+
$this->validateAndAddMethodType(self::METHOD_TYPE_CHARACTER, self::METHOD_TYPES_ALLOWED_FOR_CHARACTERS);
293+
294+
return $this->add("[^$min-$max]");
295+
}
296+
241297
/**********************************************************/
242298
/* GROUPS */
243299
/**********************************************************/
@@ -508,17 +564,38 @@ public function until($toCondition) : self
508564
/* INTERNAL METHODS */
509565
/**********************************************************/
510566

567+
/**
568+
* Escape all characters in string.
569+
*
570+
* @param string $chars
571+
* @return string
572+
*/
573+
protected function escape(string $chars)
574+
{
575+
return implode('', array_map([$this, 'escapeChar'], str_split($chars)));
576+
}
577+
511578
/**
512579
* Escape specific character.
513580
*
514581
* @param string $char
515582
* @return string
516583
*/
517-
protected function escape(string $char)
584+
protected function escapeChar(string $char)
518585
{
519586
return (strpos(static::NON_LITERAL_CHARACTERS, $char) !== false ? '\\' : '') . $char;
520587
}
521588

589+
/**
590+
* Escape '-' and ']' in string to be used in range.
591+
*
592+
* @return string
593+
*/
594+
protected function escapeRangeSpecificChars(string $chars)
595+
{
596+
return str_replace(['-', ']'], ['\\-', '\\]'], $chars);
597+
}
598+
522599
/**
523600
* Get the raw regular expression without delimiter or modifiers.
524601
*

src/Language/Helpers/Matcher.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@ class Matcher
3232
'tab' => ['class' => Methods\SimpleMethod::class, 'method' => 'tab'],
3333
'unicode' => ['class' => Methods\SimpleMethod::class, 'method' => 'unicode'],
3434
'digit' => ['class' => Methods\SimpleMethod::class, 'method' => 'digit'],
35+
'not digit' => ['class' => Methods\SimpleMethod::class, 'method' => 'notDigit'],
3536
'number' => ['class' => Methods\SimpleMethod::class, 'method' => 'digit'],
37+
'not number' => ['class' => Methods\SimpleMethod::class, 'method' => 'notDigit'],
3638
'letter' => ['class' => Methods\SimpleMethod::class, 'method' => 'letter'],
39+
'not letter' => ['class' => Methods\SimpleMethod::class, 'method' => 'notLetter'],
3740
'uppercase letter' => ['class' => Methods\SimpleMethod::class, 'method' => 'uppercaseLetter'],
41+
'not uppercase letter' => ['class' => Methods\SimpleMethod::class, 'method' => 'notUppercaseLetter'],
3842
'once' => ['class' => Methods\SimpleMethod::class, 'method' => 'once'],
3943
'twice' => ['class' => Methods\SimpleMethod::class, 'method' => 'twice'],
4044
'first match' => ['class' => Methods\SimpleMethod::class, 'method' => 'firstMatch'],
@@ -50,11 +54,16 @@ class Matcher
5054
'until' => ['class' => Methods\DefaultMethod::class, 'method' => 'until'],
5155
'raw' => ['class' => Methods\DefaultMethod::class, 'method' => 'raw'],
5256
'one of' => ['class' => Methods\DefaultMethod::class, 'method' => 'oneOf'],
57+
'not one of' => ['class' => Methods\DefaultMethod::class, 'method' => 'notOneOf'],
5358

5459
'digit from' => ['class' => Methods\ToMethod::class, 'method' => 'digit'],
60+
'not digit from' => ['class' => Methods\ToMethod::class, 'method' => 'notDigit'],
5561
'number from' => ['class' => Methods\ToMethod::class, 'method' => 'digit'],
62+
'not number from' => ['class' => Methods\ToMethod::class, 'method' => 'notDigit'],
5663
'letter from' => ['class' => Methods\ToMethod::class, 'method' => 'letter'],
64+
'not letter from' => ['class' => Methods\ToMethod::class, 'method' => 'notLetter'],
5765
'uppercase letter from' => ['class' => Methods\ToMethod::class, 'method' => 'uppercaseLetter'],
66+
'not uppercase letter from' => ['class' => Methods\ToMethod::class, 'method' => 'notUppercaseLetter'],
5867
'exactly' => ['class' => Methods\TimesMethod::class, 'method' => 'exactly'],
5968
'at least' => ['class' => Methods\TimesMethod::class, 'method' => 'atLeast'],
6069
'between' => ['class' => Methods\AndMethod::class, 'method' => 'between'],

tests/BuilderTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,29 @@ public function testRaw()
148148
{
149149
$this->assertTrue(SRL::literally('foo')->raw('b[a-z]r')->isValid());
150150
}
151+
152+
public function testNotDigit()
153+
{
154+
$regex = SRL::notDigit()
155+
->notDigit(2, 5)
156+
->get();
157+
158+
$this->assertEquals('/[^0-9][^2-5]/', $regex);
159+
}
160+
161+
public function testNotLetter()
162+
{
163+
$regex = SRL::notLetter()
164+
->notLetter('a', 'd')
165+
->get();
166+
167+
$this->assertEquals('/[^a-z][^a-d]/', $regex);
168+
}
169+
170+
public function testNotOneOf()
171+
{
172+
$regex = SRL::notOneOf('a$#')->get();
173+
174+
$this->assertEquals('/[^a\$#]/', $regex);
175+
}
151176
}

tests/ExceptionsTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public function testInvalidArgument()
128128
*/
129129
public function testInvalidExactlyArgument()
130130
{
131-
new SRL('letter exactly 2 times,not digit');
131+
new SRL('letter exactly 2 times,foo digit');
132132
}
133133

134134
/**

tests/LanguageInterpreterTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ public function testParser()
4444
$this->assertFalse($srl->isMatching('123abc'));
4545
$this->assertFalse($srl->isMatching('1a'));
4646
$this->assertFalse($srl->isMatching(''));
47+
48+
$srl = new SRL('starts with not digit, not digit from 0 to 2, not number, not number from 3 to 5 ');
49+
$this->assertEquals('/^[^0-9][^0-2][^0-9][^3-5]/', $srl->get());
50+
51+
$srl = new SRL('starts with not letter, not uppercase letter, not letter from f to o, not uppercase letter from O to z');
52+
$this->assertEquals('/^[^a-z][^A-Z][^f-o][^O-z]/', $srl->get());
53+
54+
$srl = new SRL('starts with not one of "!@#/"');
55+
$this->assertEquals('/^[^!@#\/]/', $srl->get());
4756
}
4857

4958
public function testEmail()

0 commit comments

Comments
 (0)