Skip to content

Commit 39ca92a

Browse files
Tagleliotwrobson
andauthored
Successors length limits (#224)
* Succesor length limits * Added extra test cases and comment about bug * Fix infinite loop bug when length is limited. * Re-enable disabled test * Add an edge case test for predecessor * Formatting --------- Co-authored-by: Eliot Robson <eliot.robson24@gmail.com>
1 parent 530016a commit 39ca92a

File tree

2 files changed

+66
-4
lines changed

2 files changed

+66
-4
lines changed

automata/fa/dfa.py

+55-4
Original file line numberDiff line numberDiff line change
@@ -1276,6 +1276,8 @@ def predecessor(
12761276
*,
12771277
strict: bool = True,
12781278
key: Optional[Callable[[Any], Any]] = None,
1279+
min_length: int = 0,
1280+
max_length: Optional[int] = None,
12791281
) -> Optional[str]:
12801282
"""
12811283
Returns the first string accepted by the DFA that comes before
@@ -1291,6 +1293,10 @@ def predecessor(
12911293
key : Optional[Callable], default: None
12921294
Function for defining custom lexicographical ordering. Defaults to using
12931295
the standard string ordering.
1296+
min_length : int, default: 0
1297+
Limits generation to words with at least the given length.
1298+
max_length : Optional[int], default: None
1299+
Limits generation to words with at most the given length.
12941300
12951301
Returns
12961302
------
@@ -1303,7 +1309,13 @@ def predecessor(
13031309
Raised if the language accepted by self is infinite, as we cannot
13041310
generate predecessors in this case.
13051311
"""
1306-
for word in self.predecessors(input_str, strict=strict, key=key):
1312+
for word in self.predecessors(
1313+
input_str,
1314+
strict=strict,
1315+
key=key,
1316+
min_length=min_length,
1317+
max_length=max_length,
1318+
):
13071319
return word
13081320
return None
13091321

@@ -1313,6 +1325,8 @@ def predecessors(
13131325
*,
13141326
strict: bool = True,
13151327
key: Optional[Callable[[Any], Any]] = None,
1328+
min_length: int = 0,
1329+
max_length: Optional[int] = None,
13161330
) -> Generator[str, None, None]:
13171331
"""
13181332
Generates all strings that come before the input string
@@ -1328,6 +1342,10 @@ def predecessors(
13281342
key : Optional[Callable], default: None
13291343
Function for defining custom lexicographical ordering. Defaults to using
13301344
the standard string ordering.
1345+
min_length : int, default: 0
1346+
Limits generation to words with at least the given length.
1347+
max_length : Optional[int], default: None
1348+
Limits generation to words with at most the given length.
13311349
13321350
Returns
13331351
------
@@ -1341,14 +1359,23 @@ def predecessors(
13411359
Raised if the language accepted by self is infinite, as we cannot
13421360
generate predecessors in this case.
13431361
"""
1344-
yield from self.successors(input_str, strict=strict, reverse=True, key=key)
1362+
yield from self.successors(
1363+
input_str,
1364+
strict=strict,
1365+
reverse=True,
1366+
key=key,
1367+
min_length=min_length,
1368+
max_length=max_length,
1369+
)
13451370

13461371
def successor(
13471372
self,
13481373
input_str: Optional[str],
13491374
*,
13501375
strict: bool = True,
13511376
key: Optional[Callable[[Any], Any]] = None,
1377+
min_length: int = 0,
1378+
max_length: Optional[int] = None,
13521379
) -> Optional[str]:
13531380
"""
13541381
Returns the first string accepted by the DFA that comes after
@@ -1364,13 +1391,23 @@ def successor(
13641391
key : Optional[Callable], default: None
13651392
Function for defining custom lexicographical ordering. Defaults to using
13661393
the standard string ordering.
1394+
min_length : int, default: 0
1395+
Limits generation to words with at least the given length.
1396+
max_length : Optional[int], default: None
1397+
Limits generation to words with at most the given length.
13671398
13681399
Returns
13691400
------
13701401
str
13711402
The first string accepted by the DFA lexicographically before input_string.
13721403
"""
1373-
for word in self.successors(input_str, strict=strict, key=key):
1404+
for word in self.successors(
1405+
input_str,
1406+
strict=strict,
1407+
key=key,
1408+
min_length=min_length,
1409+
max_length=max_length,
1410+
):
13741411
return word
13751412
return None
13761413

@@ -1381,6 +1418,8 @@ def successors(
13811418
strict: bool = True,
13821419
key: Optional[Callable[[Any], Any]] = None,
13831420
reverse: bool = False,
1421+
min_length: int = 0,
1422+
max_length: Optional[int] = None,
13841423
) -> Generator[str, None, None]:
13851424
"""
13861425
Generates all strings that come after the input string
@@ -1398,6 +1437,10 @@ def successors(
13981437
the standard string ordering.
13991438
reverse : bool, default: False
14001439
If True, then predecessors will be generated instead of successors.
1440+
min_length : int, default: 0
1441+
Limits generation to words with at least the given length.
1442+
max_length : Optional[int], default: None
1443+
Limits generation to words with at most the given length.
14011444
14021445
Returns
14031446
------
@@ -1446,6 +1489,8 @@ def successors(
14461489
if (
14471490
not reverse
14481491
and should_yield
1492+
and min_length <= len(char_stack)
1493+
and (max_length is None or len(char_stack) <= max_length)
14491494
and candidate == first_symbol
14501495
and state in self.final_states
14511496
):
@@ -1456,7 +1501,9 @@ def successors(
14561501
else self._get_next_current_state(state, candidate)
14571502
)
14581503
# Traverse to child if candidate is viable
1459-
if candidate_state in coaccessible_nodes:
1504+
if candidate_state in coaccessible_nodes and (
1505+
max_length is None or len(char_stack) < max_length
1506+
):
14601507
state_stack.append(candidate_state)
14611508
char_stack.append(cast(str, candidate))
14621509
candidate = first_symbol
@@ -1465,6 +1512,8 @@ def successors(
14651512
if (
14661513
reverse
14671514
and should_yield
1515+
and min_length <= len(char_stack)
1516+
and (max_length is None or len(char_stack) <= max_length)
14681517
and candidate is None
14691518
and state in self.final_states
14701519
):
@@ -1480,6 +1529,8 @@ def successors(
14801529
if (
14811530
reverse
14821531
and should_yield
1532+
and min_length <= len(char_stack)
1533+
and (max_length is None or len(char_stack) <= max_length)
14831534
and candidate is None
14841535
and state in self.final_states
14851536
):

tests/test_dfa.py

+11
Original file line numberDiff line numberDiff line change
@@ -1836,6 +1836,9 @@ def test_predecessor(self, as_partial: bool) -> None:
18361836
actual = list(dfa.predecessors("010", strict=False))
18371837

18381838
self.assertEqual(dfa.predecessor("000"), "00")
1839+
self.assertEqual(dfa.predecessor("000", max_length=1), "0")
1840+
self.assertEqual(dfa.predecessor("0", min_length=2), None)
1841+
self.assertEqual(dfa.predecessor("0000", min_length=2, max_length=3), "000")
18391842
self.assertEqual(dfa.predecessor("0100"), "010")
18401843
self.assertEqual(dfa.predecessor("1"), "010101111111101011010100")
18411844
self.assertEqual(
@@ -1872,6 +1875,10 @@ def test_successor(self, as_partial: bool) -> None:
18721875
self.assertIsNone(dfa.successor("110"))
18731876
self.assertIsNone(dfa.successor("111111110101011"))
18741877

1878+
self.assertEqual(dfa.successor("", min_length=3), "000")
1879+
self.assertEqual(dfa.successor("", min_length=4), "010101111111101011010100")
1880+
self.assertEqual(dfa.successor("010", max_length=6), "100")
1881+
18751882
infinite_dfa = DFA.from_nfa(NFA.from_regex("0*1*"))
18761883
self.assertEqual(infinite_dfa.successor(""), "0")
18771884
self.assertEqual(infinite_dfa.successor("0"), "00")
@@ -1882,6 +1889,10 @@ def test_successor(self, as_partial: bool) -> None:
18821889
self.assertEqual(infinite_dfa.successor("1"), "11")
18831890
self.assertEqual(infinite_dfa.successor(100 * "0"), 101 * "0")
18841891
self.assertEqual(infinite_dfa.successor(100 * "1"), 101 * "1")
1892+
self.assertEqual(infinite_dfa.successor("", min_length=5), "00000")
1893+
self.assertEqual(infinite_dfa.successor("000", min_length=5), "00000")
1894+
self.assertEqual(infinite_dfa.successor("1", min_length=5), "11111")
1895+
self.assertEqual(infinite_dfa.successor("1111", max_length=4), None)
18851896

18861897
@params(True, False)
18871898
def test_successor_and_predecessor(self, as_partial: bool) -> None:

0 commit comments

Comments
 (0)