Repository: TRoboto/Maha (language: python)

Issue:
Allow hierarchical parsing for numbers
### What problem are you trying to solve?

Currently, ones and tens are correctly parsed if followed by hundreds, thousands, millions, etc... This means that strings like `عشرة الاف`, `ثلاثة ملايين`, `تسعة وتسعين مليار` are extracted as they should. However strings like `مئة الف`, ` ثلاثين مليون الف`, `ثلاثمئة الف مليار` and so on are not extracted as expected. The requested feature should make this hierarchical parsing possible.

### Examples (if relevant)

```py
from maha.parsers.functions import parse_dimension
sample_text = "ثلاثة وثلاثين مئة الف"
result = parse_dimension(sample_text , numeral=True)
assert result[0].value = 3300000
```

### Definition of Done

- The order should not matter (e.g. `مئة الف` is the same as `الف مئة`).
- All possible hierarchical patterns should be implemented (e.g. `ثلاثمئة مليون`, `خمسين الف ترليون`, `واحد وعشرين الف مئة`, `مئة الف مليار`).
- Tests should be added.


Discussion / hints:

Fix patch:
diff --git a/maha/parsers/rules/duration/rule.py b/maha/parsers/rules/duration/rule.py
index 2c5d7423..323e8236 100644
--- a/maha/parsers/rules/duration/rule.py
+++ b/maha/parsers/rules/duration/rule.py
@@ -12,11 +12,13 @@
     "parse_duration",
 ]
 
-from collections import defaultdict
-from dataclasses import dataclass
 from typing import Dict, List
 
-from maha.parsers.rules.numeral.rule import RULE_NUMERAL, parse_numeral
+from maha.parsers.rules.numeral.rule import (
+    EXPRESSION_NUMERAL_MAP,
+    RULE_NUMERAL,
+    _parse_numeral,
+)
 from maha.parsers.templates import FunctionValue, Unit
 from maha.rexy import ExpressionGroup, named_group, non_capturing_group
 
@@ -110,70 +112,49 @@ def get_matched_value(matched_text) -> ValueUnit:
     return ValueUnit(value=exp_val.value, unit=exp_val.unit)
 
 
-@dataclass
-class DurationParsedValue:
-    name: str
-    text: str
-    index: int
-    values: Dict[str, List]
-
-    def __init__(self, name, text, index) -> None:
-        self.name = name
-        self.text = text
-        self.index = index
-        self.values = defaultdict(list)
-
-    def capturesdict(self):
-        return self.values
-
-
-class IndexPriorityList:
-    def __init__(self):
-        self.items: List[DurationParsedValue] = []
-
-    def add_item(self, name, text, index):
-        self.items.append(DurationParsedValue(name, text, index))
-        self.items.sort(key=lambda i: i.index)
-
-    def add_value(self, name, value, index) -> None:
-        for item in self.items:
-            if index < item.index:
-                item.values[name].append(value)
-                break
+def get_groups():
+    return [
+        "seconds",
+        "minutes",
+        "hours",
+        "days",
+        "weeks",
+        "months",
+        "years",
+    ]
 
 
 def parse_duration(match):
     """Parse duration."""
     groups = match.capturesdict()
     groups_keys = list(groups)
-    collection = IndexPriorityList()
-    for key in ["seconds", "minutes", "hours", "days", "weeks", "months", "years"]:
-        if key not in groups:
+
+    duration_groups = get_groups()
+    sorted_values = {}
+    for group in list(EXPRESSION_NUMERAL_MAP) + duration_groups:
+        if group not in groups_keys:
             continue
-        for i, t in enumerate(match.starts(groups_keys.index(key) + 1)):
-            collection.add_item(key, groups[key][i], t)
-
-    for key in [
-        "trillions",
-        "billions",
-        "millions",
-        "thousands",
-        "hundreds",
-        "decimals",
-        "tens",
-        "ones",
-        "integers",
-    ]:
-        for i, t in enumerate(match.starts(groups_keys.index(key) + 1)):
-            collection.add_value(key, groups[key][i], t)
+        for i, value in enumerate(groups.get(group)):
+            index = match.starts(groups_keys.index(group) + 1)[i]
+            sorted_values[index] = {"group": group, "value": value}
+
+    sorted_values = dict(sorted(sorted_values.items()))
 
+    # holds numeral values before a unit
+    temp_dict = {}
     values = []
-    for item in collection.items:
-        numeral = parse_numeral(item)
-        valueunit = get_matched_value(item.text)
+    for index, item in sorted_values.items():
+        group = item["group"]
+        if group not in duration_groups:
+            temp_dict[index] = item
+            continue
+        numeral = _parse_numeral(temp_dict)
+        valueunit = get_matched_value(item["value"])
         if numeral:
             valueunit.value = numeral
         values.append(valueunit)
+        temp_dict = {}
+
     for item in groups.get("fractions", []):
         values.append(get_unit_fraction_value(item))
 
diff --git a/maha/parsers/rules/numeral/rule.py b/maha/parsers/rules/numeral/rule.py
index 21498466..109d2b8d 100644
--- a/maha/parsers/rules/numeral/rule.py
+++ b/maha/parsers/rules/numeral/rule.py
@@ -8,11 +8,9 @@
     "RULE_NUMERAL_TRILLIONS",
     "RULE_NUMERAL_INTEGERS",
     "RULE_NUMERAL",
-    "parse_numeral",
 ]
 
-
-from maha.expressions import EXPRESSION_DECIMAL, EXPRESSION_INTEGER
+from maha.expressions import EXPRESSION_DECIMAL, EXPRESSION_INTEGER, EXPRESSION_SPACE
 from maha.parsers.templates import FunctionValue
 from maha.parsers.utils import convert_to_number_if_possible
 from maha.rexy import (
@@ -23,160 +21,113 @@
 )
 
 from ..common import (
-    FRACTIONS,
+    HALF,
+    QUARTER,
+    THIRD,
+    THREE_QUARTERS,
+    TWO_THIRDS,
     WAW_CONNECTOR,
     combine_patterns,
-    get_fractions_of_unit_pattern,
     spaced_patterns,
 )
 from .values import *
 
+NUMERAL_VALUES_GROUP_NAME = "numeral_values"
+MULTIPLIERS_GROUP_NAME = "multipliers"
+DECIMAL_PART_GROUP_NAME = "decimal_part"
 
-def get_combined_value(groups, multiplier="") -> float:
-
-    singular = globals().get("ONE_" + multiplier[:-1].upper())
-    dual = globals().get("TWO_" + multiplier.upper())
-    plural = globals().get("SEVERAL_" + multiplier.upper())
-    value = 0
-    for group in groups:
-        mul = get_matched_value(group, singular, dual)
-        if mul:
-            value += mul
-        else:
-            value += parse_combined(group, singular, plural)
-    return value
 
+def numeral_group(pattern):
+    return named_group(NUMERAL_VALUES_GROUP_NAME, pattern)
 
-def get_matched_value(matched_text, dual, singular):
-    if not (dual or singular):
-        return
-    if dual.fullmatch(matched_text):
-        return dual.value
-    if singular.fullmatch(matched_text):
-        return singular.value
 
+def _construct_numeral(sorted_values) -> float:
+    output = [0] * len(sorted_values)
+    last_numeral_index = 0
+    multiply = False
+    for i, (_, dict_value) in enumerate(sorted_values.items()):
+        group = dict_value["group"]
+        exp = EXPRESSION_NUMERAL_MAP[group].get_matched_expression(dict_value["value"])
+        assert exp is not None
+        value = next(iter(exp(dict_value["value"]))).value
+        if group == NUMERAL_VALUES_GROUP_NAME:
+            if multiply:
+                output[i] *= value
+                multiply = False
+            else:
+                output[i] += value
+            last_numeral_index = i
+        elif group == "before_fractions":
+            output[i + 1] = value
+            multiply = True
+        elif group == MULTIPLIERS_GROUP_NAME:
+            output[last_numeral_index] *= value
+        elif group == "after_fraction":
+            output[last_numeral_index] *= value
 
-def parse_ones(text):
-    exp = ones.get_matched_expression(text)
-    if exp:
-        return exp.value  # type: ignore
+    total = sum(output)
+    # to int if possible
+    if total == int(total):
+        total = int(total)
+    return total
 
 
-def parse_tens(matched_text):
-    waw = WAW_CONNECTOR.search(matched_text)
-    if waw:
-        _ones, _tens = matched_text.split(waw.group(0))
-        value = (
-            ones.get_matched_expression(_ones).value  # type: ignore
-            + perfect_tens.get_matched_expression(_tens).value  # type: ignore
+def _parse_numeral(sorted_values):
+    decimal_part_index = 0
+    for k, v in sorted_values.items():
+        if DECIMAL_PART_GROUP_NAME == v["group"]:
+            decimal_part_index = k
+    if decimal_part_index:
+        integer = _construct_numeral(
+            {k: v for k, v in sorted_values.items() if k < decimal_part_index}
         )
-        return value
-    exp = perfect_tens.get_matched_expression(matched_text)
-    if not exp:
-        exp = eleven_to_nineteen.get_matched_expression(matched_text)
-    if not exp and TEN.match(matched_text):
-        return TEN.value
-
-    if exp:
-        return exp.value  # type: ignore
-
-
-def parse_combined(matched_text, singular=None, plural=None):
-    value = 1
-    if singular and plural:
-        multiplier = plural.search(matched_text) or singular.search(matched_text)
-        matched_text = matched_text.replace(multiplier.group(0), "").strip()
-        value = singular.value
-
-    number = EXPRESSION_DECIMAL.fullmatch(matched_text) or EXPRESSION_INTEGER.fullmatch(
-        matched_text
-    )
-    if number is not None:
-        number = convert_to_number_if_possible(number.group())
-    tens = parse_tens(matched_text)
-    ones = parse_ones(matched_text)
-    fraction = FRACTIONS.get_matched_expression(matched_text)
-    if number is not None:
-        value *= number
-    elif tens is not None:
-        value *= tens
-    elif ones is not None:
-        value *= ones
-    elif fraction is not None:
-        value *= fraction.value  # type: ignore
-    return value
-
-
-def get_groups():
-    return [
-        "trillions",
-        "billions",
-        "millions",
-        "thousands",
-        "hundreds",
-        "tens",
-        "ones",
-        "integers",
-        "decimals",
-    ]
-
+        decimal = _construct_numeral(
+            {k: v for k, v in sorted_values.items() if k > decimal_part_index}
+        )
+        return integer + decimal / 10 ** len(str(decimal))
 
-def _parse_numeral_part(groups):
-    value = 0
-    for group in get_groups():
-        values = groups.get(group)
-        if not values:
-            continue
-        value += get_combined_value(values, group)
-    return value
+    return _construct_numeral(sorted_values)
 
 
 def parse_numeral(match):
     groups = match.capturesdict()
     groups_keys = list(groups)
 
-    if groups.get("decimal_part"):
-        decimal_start_index = match.starts(groups_keys.index("decimal_part") + 1)[0]
-        integer = 0
-        decimal = 0
-        for group in get_groups():
-            g_start = match.starts(groups_keys.index(group) + 1)
-            if group not in groups_keys or not g_start:
-                continue
-            for m_start, m_group in zip(g_start, groups[group]):
-                if m_start < decimal_start_index:
-                    integer += get_combined_value([m_group], group)
-                else:
-                    decimal += get_combined_value([m_group], group)
-        return integer + decimal / 10 ** len(str(decimal))
-
-    return _parse_numeral_part(groups)
+    if not groups_keys:
+        return
 
+    sorted_values = {}
+    for group in EXPRESSION_NUMERAL_MAP:
+        if group not in groups_keys:
+            continue
+        for i, value in enumerate(groups.get(group)):
+            index = match.starts(groups_keys.index(group) + 1)[i]
+            sorted_values[index] = {"group": group, "value": value}
+    # sort by index
+    sorted_values = dict(sorted(sorted_values.items()))
 
-def get_patterns(one, two, several):
-    return non_capturing_group(
-        spaced_patterns(EXPRESSION_DECIMAL, non_capturing_group(one, several)),
-        spaced_patterns(EXPRESSION_INTEGER, non_capturing_group(one, several)),
-        spaced_patterns(tens, non_capturing_group(one, several)),
-        spaced_patterns(ones.join(), non_capturing_group(one, several)),
-        get_fractions_of_unit_pattern(str(one)),
-        two,
-        one,
-    )
+    return _parse_numeral(sorted_values)
 
 
 ones = ExpressionGroup(ZERO, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE)
 perfect_tens = ExpressionGroup(
     TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, EIGHTY, NINETY
 )
-combined_tens = ones.join() + WAW_CONNECTOR + perfect_tens.join()
 eleven_to_nineteen = ExpressionGroup(
     ELEVEN, TWELVE, THIRTEEN, FOURTEEN, FIFTEEN, SIXTEEN, SEVENTEEN, EIGHTEEN, NINETEEN
 )
-tens = non_capturing_group(
-    perfect_tens.join(),
-    combined_tens,
-    eleven_to_nineteen.join(),
+
+COMBINED_TENS = FunctionValue(
+    lambda match: ones.get_matched_expression(match.group("ones")).value  # type: ignore
+    + perfect_tens.get_matched_expression(match.group("tens")).value,  # type: ignore
+    named_group("ones", ones.join())
+    + WAW_CONNECTOR
+    + named_group("tens", perfect_tens.join()),
+)
+TENS = ExpressionGroup(
+    perfect_tens,
+    COMBINED_TENS,
+    eleven_to_nineteen,
     TEN,
 )
 perfect_hundreds = ExpressionGroup(
@@ -190,91 +141,217 @@ def get_patterns(one, two, several):
     EIGHT_HUNDREDS,
     NINE_HUNDREDS,
 )
-ones_group = named_group("ones", ones.join())
-tens_group = named_group("tens", tens)
-hundreds_group = named_group(
-    "hundreds",
-    non_capturing_group(
-        perfect_hundreds[2:].join(),
-        get_patterns(ONE_HUNDRED, TWO_HUNDREDS, SEVERAL_HUNDREDS),
-    ),
+RULE_NUMERAL_INTEGERS = FunctionValue(
+    lambda match: convert_to_number_if_possible(str(match.group())),
+    str(EXPRESSION_INTEGER),
 )
-thousands_group = named_group(
-    "thousands", get_patterns(ONE_THOUSAND, TWO_THOUSANDS, SEVERAL_THOUSANDS)
+RULE_NUMERAL_DECIMALS = FunctionValue(
+    lambda match: convert_to_number_if_possible(str(match.group())),
+    str(EXPRESSION_DECIMAL),
 )
-millions_group = named_group(
-    "millions", get_patterns(ONE_MILLION, TWO_MILLIONS, SEVERAL_MILLIONS)
+MULTIPLIERS = ExpressionGroup(
+    ONE_HUNDRED,
+    ONE_THOUSAND,
+    ONE_MILLION,
+    ONE_BILLION,
+    ONE_TRILLION,
+    SEVERAL_HUNDREDS,
+    SEVERAL_THOUSANDS,
+    SEVERAL_MILLIONS,
+    SEVERAL_BILLIONS,
+    SEVERAL_TRILLIONS,
 )
-billions_group = named_group(
-    "billions", get_patterns(ONE_BILLION, TWO_BILLIONS, SEVERAL_BILLIONS)
+NUMERAL_COMMON_VALUES = ExpressionGroup(
+    COMBINED_TENS,
+    perfect_tens,
+    eleven_to_nineteen,
+    TEN,
+    ones,
+    RULE_NUMERAL_DECIMALS,
+    RULE_NUMERAL_INTEGERS,
 )
-trillions_group = named_group(
-    "trillions", get_patterns(ONE_TRILLION, TWO_TRILLIONS, SEVERAL_TRILLIONS)
+NUMERAL_VALUES = ExpressionGroup(
+    TWO_HUNDREDS,
+    TWO_THOUSANDS,
+    TWO_MILLIONS,
+    TWO_BILLIONS,
+    TWO_TRILLIONS,
+    perfect_hundreds,
+    # ONE_HUNDRED, Already defined in perfect_hundreds
+    ONE_THOUSAND,
+    ONE_MILLION,
+    ONE_BILLION,
+    ONE_TRILLION,
+    COMBINED_TENS,
+    perfect_tens,
+    eleven_to_nineteen,
+    TEN,
+    ones,
+    RULE_NUMERAL_DECIMALS,
+    RULE_NUMERAL_INTEGERS,
 )
+BEFORE_FRACTIONS = ExpressionGroup(HALF, THIRD, QUARTER)
+AFTER_FRACTION = ExpressionGroup(THREE_QUARTERS, TWO_THIRDS)
 
-integer_group = named_group("integers", str(EXPRESSION_INTEGER))
-decimal_group = named_group("decimals", str(EXPRESSION_DECIMAL))
+before_fractions_group = named_group("before_fractions", BEFORE_FRACTIONS.join())
+after_fraction_group = named_group("after_fraction", AFTER_FRACTION.join())
 
-RULE_NUMERAL_ONES = FunctionValue(parse_numeral, ones_group)
-RULE_NUMERAL_TENS = FunctionValue(parse_numeral, tens_group)
+
+def get_pattern(
+    numeral_exp_group: ExpressionGroup, multipliers_exp_group: ExpressionGroup
+) -> str:
+    pattern = non_capturing_group(
+        optional_non_capturing_group(before_fractions_group + EXPRESSION_SPACE)
+        + named_group(NUMERAL_VALUES_GROUP_NAME, numeral_exp_group.join())
+        + non_capturing_group(
+            WAW_CONNECTOR
+            + named_group(NUMERAL_VALUES_GROUP_NAME, numeral_exp_group.join())
+        )
+        + "*"
+        + optional_non_capturing_group(EXPRESSION_SPACE + after_fraction_group)
+        + non_capturing_group(
+            EXPRESSION_SPACE
+            + named_group(MULTIPLIERS_GROUP_NAME, multipliers_exp_group.join())
+        )
+        + "*"
+    )
+    return pattern
+
+
+RULE_NUMERAL_ONES = FunctionValue(
+    parse_numeral, named_group(NUMERAL_VALUES_GROUP_NAME, ones.join())
+)
+RULE_NUMERAL_TENS = FunctionValue(
+    parse_numeral, named_group(NUMERAL_VALUES_GROUP_NAME, TENS.join())
+)
 RULE_NUMERAL_HUNDREDS = FunctionValue(
-    parse_numeral, combine_patterns(hundreds_group, tens_group, ones_group)
+    parse_numeral,
+    combine_patterns(
+        get_pattern(
+            ExpressionGroup(TWO_HUNDREDS, perfect_hundreds),
+            ExpressionGroup(SEVERAL_HUNDREDS, ONE_HUNDRED),
+        ),
+        RULE_NUMERAL_TENS,
+        RULE_NUMERAL_ONES,
+    ),
 )
 RULE_NUMERAL_THOUSANDS = FunctionValue(
     parse_numeral,
-    combine_patterns(thousands_group, hundreds_group, tens_group, ones_group),
+    combine_patterns(
+        get_pattern(
+            ExpressionGroup(TWO_THOUSANDS, ONE_THOUSAND),
+            ExpressionGroup(SEVERAL_THOUSANDS, ONE_THOUSAND),
+        ),
+        get_pattern(
+            ExpressionGroup(TWO_HUNDREDS, perfect_hundreds),
+            ExpressionGroup(SEVERAL_HUNDREDS, ONE_HUNDRED),
+        ),
+        RULE_NUMERAL_TENS,
+        RULE_NUMERAL_ONES,
+    ),
 )
 RULE_NUMERAL_MILLIONS = FunctionValue(
     parse_numeral,
     combine_patterns(
-        millions_group, thousands_group, hundreds_group, tens_group, ones_group
+        get_pattern(
+            ExpressionGroup(TWO_MILLIONS, ONE_MILLION),
+            ExpressionGroup(SEVERAL_MILLIONS, ONE_MILLION),
+        ),
+        get_pattern(
+            ExpressionGroup(
+                TWO_HUNDREDS, perfect_hundreds, TWO_THOUSANDS, ONE_THOUSAND
+            ),
+            ExpressionGroup(
+                SEVERAL_HUNDREDS, ONE_HUNDRED, SEVERAL_THOUSANDS, ONE_THOUSAND
+            ),
+        ),
+        RULE_NUMERAL_TENS,
+        RULE_NUMERAL_ONES,
     ),
 )
 RULE_NUMERAL_BILLIONS = FunctionValue(
     parse_numeral,
     combine_patterns(
-        billions_group,
-        millions_group,
-        thousands_group,
-        hundreds_group,
-        tens_group,
-        ones_group,
+        get_pattern(
+            ExpressionGroup(TWO_BILLIONS, ONE_BILLION),
+            ExpressionGroup(SEVERAL_BILLIONS, ONE_BILLION),
+        ),
+        get_pattern(
+            ExpressionGroup(
+                TWO_HUNDREDS,
+                perfect_hundreds,
+                TWO_THOUSANDS,
+                ONE_THOUSAND,
+                TWO_MILLIONS,
+                ONE_MILLION,
+            ),
+            ExpressionGroup(
+                SEVERAL_HUNDREDS,
+                ONE_HUNDRED,
+                SEVERAL_THOUSANDS,
+                ONE_THOUSAND,
+                SEVERAL_MILLIONS,
+                ONE_MILLION,
+            ),
+        ),
+        RULE_NUMERAL_TENS,
+        RULE_NUMERAL_ONES,
     ),
 )
 RULE_NUMERAL_TRILLIONS = FunctionValue(
     parse_numeral,
     combine_patterns(
-        trillions_group,
-        billions_group,
-        millions_group,
-        thousands_group,
-        hundreds_group,
-        tens_group,
-        ones_group,
+        get_pattern(
+            ExpressionGroup(TWO_TRILLIONS, ONE_TRILLION),
+            ExpressionGroup(SEVERAL_TRILLIONS, ONE_TRILLION),
+        ),
+        get_pattern(
+            ExpressionGroup(
+                TWO_HUNDREDS,
+                perfect_hundreds,
+                TWO_THOUSANDS,
+                ONE_THOUSAND,
+                TWO_MILLIONS,
+                ONE_MILLION,
+                TWO_BILLIONS,
+                ONE_BILLION,
+            ),
+            ExpressionGroup(
+                SEVERAL_HUNDREDS,
+                ONE_HUNDRED,
+                SEVERAL_THOUSANDS,
+                ONE_THOUSAND,
+                SEVERAL_MILLIONS,
+                ONE_MILLION,
+                SEVERAL_BILLIONS,
+                ONE_BILLION,
+            ),
+        ),
+        RULE_NUMERAL_TENS,
+        RULE_NUMERAL_ONES,
     ),
 )
-RULE_NUMERAL_INTEGERS = FunctionValue(parse_numeral, integer_group)
 
-_all_time_expressions_pattern = combine_patterns(
-    trillions_group,
-    billions_group,
-    millions_group,
-    thousands_group,
-    hundreds_group,
-    tens_group,
-    ones_group,
-    decimal_group,
-    integer_group,
-    combine_all=True,
+_numeral_numeral_pattern = get_pattern(NUMERAL_VALUES, MULTIPLIERS)
+_all_numeral_numeral_pattern = combine_patterns(
+    _numeral_numeral_pattern, seperator=WAW_CONNECTOR
 )
 RULE_NUMERAL = FunctionValue(
     parse_numeral,
-    named_group("integer_part", _all_time_expressions_pattern)
+    _all_numeral_numeral_pattern
     + optional_non_capturing_group(
         named_group(
-            "decimal_part",
+            DECIMAL_PART_GROUP_NAME,
             EXPRESSION_SPACE
-            + spaced_patterns(EXPRESSION_OF_FASILA, _all_time_expressions_pattern),
-        ),
+            + spaced_patterns(EXPRESSION_OF_FASILA, _all_numeral_numeral_pattern),
+        )
     ),
 )
+
+EXPRESSION_NUMERAL_MAP = {
+    "before_fractions": BEFORE_FRACTIONS,
+    "after_fraction": AFTER_FRACTION,
+    NUMERAL_VALUES_GROUP_NAME: NUMERAL_VALUES,
+    MULTIPLIERS_GROUP_NAME: MULTIPLIERS,
+    DECIMAL_PART_GROUP_NAME: ExpressionGroup(),
+}
diff --git a/maha/parsers/rules/numeral/values.py b/maha/parsers/rules/numeral/values.py
index 7fbbdff3..02735366 100644
--- a/maha/parsers/rules/numeral/values.py
+++ b/maha/parsers/rules/numeral/values.py
@@ -1,4 +1,4 @@
-from maha.expressions import EXPRESSION_SPACE, EXPRESSION_SPACE_OR_NONE
+from maha.expressions import EXPRESSION_SPACE_OR_NONE
 from maha.parsers.templates import Value
 from maha.rexy import Expression, non_capturing_group
 
diff --git a/maha/parsers/rules/time/values.py b/maha/parsers/rules/time/values.py
index f4f60603..2140a656 100644
--- a/maha/parsers/rules/time/values.py
+++ b/maha/parsers/rules/time/values.py
@@ -30,7 +30,6 @@
     RULE_NUMERAL_THOUSANDS,
     TEN,
     eleven_to_nineteen,
-    parse_numeral,
 )
 from maha.parsers.rules.numeral.values import TEH_OPTIONAL_SUFFIX
 from maha.parsers.rules.ordinal.rule import (
@@ -117,10 +116,8 @@ def parse_value(value: dict) -> TimeValue:
     RULE_NUMERAL_TENS, RULE_NUMERAL_ONES, RULE_NUMERAL_INTEGERS
 )
 numeral_hours = ExpressionGroup(
-    FunctionValue(
-        parse_numeral,
-        named_group("tens", non_capturing_group(eleven_to_nineteen.join(), TEN)),
-    ),
+    eleven_to_nineteen,
+    TEN,
     RULE_NUMERAL_ONES,
     RULE_NUMERAL_INTEGERS,
 )
@@ -145,7 +142,7 @@ def parse_value(value: dict) -> TimeValue:
 # ----------------------------------------------------
 numeral_thousands = ExpressionGroup(
     RULE_NUMERAL_THOUSANDS,
-    FunctionValue(lambda match: int(match.group(0)), named_group("integers", r"\d{4}")),
+    FunctionValue(lambda match: int(match.group()), r"\d{4}"),
 )
 ordinal_thousands = ExpressionGroup(RULE_ORDINAL_THOUSANDS)
 NUMERAL_YEAR = FunctionValue(
diff --git a/maha/rexy/cache/1052453576738913180.crp b/maha/rexy/cache/1052453576738913180.crp
new file mode 100644
index 00000000..a48cc7da
Binary files /dev/null and b/maha/rexy/cache/1052453576738913180.crp differ
diff --git a/maha/rexy/cache/10592773813688160.crp b/maha/rexy/cache/10592773813688160.crp
deleted file mode 100644
index 4a399e94..00000000
Binary files a/maha/rexy/cache/10592773813688160.crp and /dev/null differ
diff --git a/maha/rexy/cache/110775108375901181.crp b/maha/rexy/cache/110775108375901181.crp
index c98f7a7c..8549204a 100644
Binary files a/maha/rexy/cache/110775108375901181.crp and b/maha/rexy/cache/110775108375901181.crp differ
diff --git a/maha/rexy/cache/1114935795093392389.crp b/maha/rexy/cache/1114935795093392389.crp
deleted file mode 100644
index 7728c1c8..00000000
Binary files a/maha/rexy/cache/1114935795093392389.crp and /dev/null differ
diff --git a/maha/rexy/cache/1115026399872818091.crp b/maha/rexy/cache/1115026399872818091.crp
index d8fa94e0..4a9b5f3b 100644
Binary files a/maha/rexy/cache/1115026399872818091.crp and b/maha/rexy/cache/1115026399872818091.crp differ
diff --git a/maha/rexy/cache/112390726106408012.crp b/maha/rexy/cache/112390726106408012.crp
index 51b843ad..f6f81d61 100644
Binary files a/maha/rexy/cache/112390726106408012.crp and b/maha/rexy/cache/112390726106408012.crp differ
diff --git a/maha/rexy/cache/570839184358471311.crp b/maha/rexy/cache/1138613943936431582.crp
similarity index 60%
rename from maha/rexy/cache/570839184358471311.crp
rename to maha/rexy/cache/1138613943936431582.crp
index 072b607f..ab282b1e 100644
Binary files a/maha/rexy/cache/570839184358471311.crp and b/maha/rexy/cache/1138613943936431582.crp differ
diff --git a/maha/rexy/cache/1204972646928817248.crp b/maha/rexy/cache/1204972646928817248.crp
deleted file mode 100644
index f0cd71cc..00000000
Binary files a/maha/rexy/cache/1204972646928817248.crp and /dev/null differ
diff --git a/maha/rexy/cache/1219393583345720904.crp b/maha/rexy/cache/1219393583345720904.crp
deleted file mode 100644
index fcb64c19..00000000
Binary files a/maha/rexy/cache/1219393583345720904.crp and /dev/null differ
diff --git a/maha/rexy/cache/1377885468330203896.crp b/maha/rexy/cache/1221597473455654214.crp
similarity index 51%
rename from maha/rexy/cache/1377885468330203896.crp
rename to maha/rexy/cache/1221597473455654214.crp
index a819a312..029c8ec6 100644
Binary files a/maha/rexy/cache/1377885468330203896.crp and b/maha/rexy/cache/1221597473455654214.crp differ
diff --git a/maha/rexy/cache/1228620432251142528.crp b/maha/rexy/cache/1228620432251142528.crp
index 08147d39..3494686e 100644
Binary files a/maha/rexy/cache/1228620432251142528.crp and b/maha/rexy/cache/1228620432251142528.crp differ
diff --git a/maha/rexy/cache/1239422574153693596.crp b/maha/rexy/cache/1239422574153693596.crp
index 7522c2b1..a6b17470 100644
Binary files a/maha/rexy/cache/1239422574153693596.crp and b/maha/rexy/cache/1239422574153693596.crp differ
diff --git a/maha/rexy/cache/1241139703575746635.crp b/maha/rexy/cache/1241139703575746635.crp
new file mode 100644
index 00000000..8a4b92f6
Binary files /dev/null and b/maha/rexy/cache/1241139703575746635.crp differ
diff --git a/maha/rexy/cache/1263142815486565862.crp b/maha/rexy/cache/1263142815486565862.crp
index 190f2461..edd9fc53 100644
Binary files a/maha/rexy/cache/1263142815486565862.crp and b/maha/rexy/cache/1263142815486565862.crp differ
diff --git a/maha/rexy/cache/1287928660501231175.crp b/maha/rexy/cache/1287928660501231175.crp
index 6daf3b24..9b885115 100644
Binary files a/maha/rexy/cache/1287928660501231175.crp and b/maha/rexy/cache/1287928660501231175.crp differ
diff --git a/maha/rexy/cache/1302827994990440501.crp b/maha/rexy/cache/1302827994990440501.crp
new file mode 100644
index 00000000..30ec8461
Binary files /dev/null and b/maha/rexy/cache/1302827994990440501.crp differ
diff --git a/maha/rexy/cache/1315646384786162959.crp b/maha/rexy/cache/1315646384786162959.crp
deleted file mode 100644
index 870aa463..00000000
Binary files a/maha/rexy/cache/1315646384786162959.crp and /dev/null differ
diff --git a/maha/rexy/cache/1316525134630011113.crp b/maha/rexy/cache/1316525134630011113.crp
index df8a3eb4..354b50b4 100644
Binary files a/maha/rexy/cache/1316525134630011113.crp and b/maha/rexy/cache/1316525134630011113.crp differ
diff --git a/maha/rexy/cache/1317921263792624166.crp b/maha/rexy/cache/1317921263792624166.crp
new file mode 100644
index 00000000..11769037
Binary files /dev/null and b/maha/rexy/cache/1317921263792624166.crp differ
diff --git a/maha/rexy/cache/1337882897878117478.crp b/maha/rexy/cache/1337882897878117478.crp
deleted file mode 100644
index 67d8426e..00000000
Binary files a/maha/rexy/cache/1337882897878117478.crp and /dev/null differ
diff --git a/maha/rexy/cache/1371307604844998554.crp b/maha/rexy/cache/1371307604844998554.crp
deleted file mode 100644
index 2c35e540..00000000
Binary files a/maha/rexy/cache/1371307604844998554.crp and /dev/null differ
diff --git a/maha/rexy/cache/1385110758750252266.crp b/maha/rexy/cache/1385110758750252266.crp
new file mode 100644
index 00000000..2c96758e
Binary files /dev/null and b/maha/rexy/cache/1385110758750252266.crp differ
diff --git a/maha/rexy/cache/1396135611345379101.crp b/maha/rexy/cache/1396135611345379101.crp
new file mode 100644
index 00000000..78f59c36
Binary files /dev/null and b/maha/rexy/cache/1396135611345379101.crp differ
diff --git a/maha/rexy/cache/1397711368195516970.crp b/maha/rexy/cache/1397711368195516970.crp
index 12e955ff..8871eb42 100644
Binary files a/maha/rexy/cache/1397711368195516970.crp and b/maha/rexy/cache/1397711368195516970.crp differ
diff --git a/maha/rexy/cache/1419787635848122052.crp b/maha/rexy/cache/1419787635848122052.crp
deleted file mode 100644
index ac0ea041..00000000
Binary files a/maha/rexy/cache/1419787635848122052.crp and /dev/null differ
diff --git a/maha/rexy/cache/1497701781310189929.crp b/maha/rexy/cache/1497701781310189929.crp
deleted file mode 100644
index ce1a042d..00000000
Binary files a/maha/rexy/cache/1497701781310189929.crp and /dev/null differ
diff --git a/maha/rexy/cache/135423976563509588.crp b/maha/rexy/cache/1502850660891414699.crp
similarity index 50%
rename from maha/rexy/cache/135423976563509588.crp
rename to maha/rexy/cache/1502850660891414699.crp
index 411662ed..fe4cbcb9 100644
Binary files a/maha/rexy/cache/135423976563509588.crp and b/maha/rexy/cache/1502850660891414699.crp differ
diff --git a/maha/rexy/cache/1293701547430760479.crp b/maha/rexy/cache/1503571440529053360.crp
similarity index 54%
rename from maha/rexy/cache/1293701547430760479.crp
rename to maha/rexy/cache/1503571440529053360.crp
index d9eabbb2..4eb86953 100644
Binary files a/maha/rexy/cache/1293701547430760479.crp and b/maha/rexy/cache/1503571440529053360.crp differ
diff --git a/maha/rexy/cache/2191821222224644333.crp b/maha/rexy/cache/152249947335763101.crp
similarity index 53%
rename from maha/rexy/cache/2191821222224644333.crp
rename to maha/rexy/cache/152249947335763101.crp
index f1ba05f0..2ac14613 100644
Binary files a/maha/rexy/cache/2191821222224644333.crp and b/maha/rexy/cache/152249947335763101.crp differ
diff --git a/maha/rexy/cache/1544319115645667724.crp b/maha/rexy/cache/1544319115645667724.crp
new file mode 100644
index 00000000..a1777dfc
Binary files /dev/null and b/maha/rexy/cache/1544319115645667724.crp differ
diff --git a/maha/rexy/cache/1552811132799082124.crp b/maha/rexy/cache/1552811132799082124.crp
index e4ee94bf..97712527 100644
Binary files a/maha/rexy/cache/1552811132799082124.crp and b/maha/rexy/cache/1552811132799082124.crp differ
diff --git a/maha/rexy/cache/1558672610053107902.crp b/maha/rexy/cache/1558672610053107902.crp
deleted file mode 100644
index 87f973d6..00000000
Binary files a/maha/rexy/cache/1558672610053107902.crp and /dev/null differ
diff --git a/maha/rexy/cache/157303998004443867.crp b/maha/rexy/cache/157303998004443867.crp
deleted file mode 100644
index 01c1fedf..00000000
Binary files a/maha/rexy/cache/157303998004443867.crp and /dev/null differ
diff --git a/maha/rexy/cache/1591921008478992991.crp b/maha/rexy/cache/1591921008478992991.crp
deleted file mode 100644
index 3302033e..00000000
Binary files a/maha/rexy/cache/1591921008478992991.crp and /dev/null differ
diff --git a/maha/rexy/cache/331317589382532808.crp b/maha/rexy/cache/1632644775859620941.crp
similarity index 53%
rename from maha/rexy/cache/331317589382532808.crp
rename to maha/rexy/cache/1632644775859620941.crp
index 74bfd81f..80648d72 100644
Binary files a/maha/rexy/cache/331317589382532808.crp and b/maha/rexy/cache/1632644775859620941.crp differ
diff --git a/maha/rexy/cache/1633350640308466769.crp b/maha/rexy/cache/1633350640308466769.crp
new file mode 100644
index 00000000..e2d72061
Binary files /dev/null and b/maha/rexy/cache/1633350640308466769.crp differ
diff --git a/maha/rexy/cache/1643102283079414381.crp b/maha/rexy/cache/1643102283079414381.crp
deleted file mode 100644
index 79a8469b..00000000
Binary files a/maha/rexy/cache/1643102283079414381.crp and /dev/null differ
diff --git a/maha/rexy/cache/1648414358024233921.crp b/maha/rexy/cache/1648414358024233921.crp
index e2397254..dcfac8d2 100644
Binary files a/maha/rexy/cache/1648414358024233921.crp and b/maha/rexy/cache/1648414358024233921.crp differ
diff --git a/maha/rexy/cache/1695699768931161527.crp b/maha/rexy/cache/1695699768931161527.crp
deleted file mode 100644
index e139c495..00000000
Binary files a/maha/rexy/cache/1695699768931161527.crp and /dev/null differ
diff --git a/maha/rexy/cache/1700288154582776020.crp b/maha/rexy/cache/1700288154582776020.crp
new file mode 100644
index 00000000..9dbd56b5
Binary files /dev/null and b/maha/rexy/cache/1700288154582776020.crp differ
diff --git a/maha/rexy/cache/247425006177863124.crp b/maha/rexy/cache/170356429727092291.crp
similarity index 54%
rename from maha/rexy/cache/247425006177863124.crp
rename to maha/rexy/cache/170356429727092291.crp
index b6b90338..e70ab825 100644
Binary files a/maha/rexy/cache/247425006177863124.crp and b/maha/rexy/cache/170356429727092291.crp differ
diff --git a/maha/rexy/cache/1732911261814690984.crp b/maha/rexy/cache/1732911261814690984.crp
index 00192b22..abc8e48d 100644
Binary files a/maha/rexy/cache/1732911261814690984.crp and b/maha/rexy/cache/1732911261814690984.crp differ
diff --git a/maha/rexy/cache/1738471189277925002.crp b/maha/rexy/cache/1738471189277925002.crp
deleted file mode 100644
index c87fa6ec..00000000
Binary files a/maha/rexy/cache/1738471189277925002.crp and /dev/null differ
diff --git a/maha/rexy/cache/1776654182686057967.crp b/maha/rexy/cache/1776654182686057967.crp
new file mode 100644
index 00000000..2a4fa600
Binary files /dev/null and b/maha/rexy/cache/1776654182686057967.crp differ
diff --git a/maha/rexy/cache/177875555741033717.crp b/maha/rexy/cache/1785135393771375979.crp
similarity index 54%
rename from maha/rexy/cache/177875555741033717.crp
rename to maha/rexy/cache/1785135393771375979.crp
index e5655b34..36da9199 100644
Binary files a/maha/rexy/cache/177875555741033717.crp and b/maha/rexy/cache/1785135393771375979.crp differ
diff --git a/maha/rexy/cache/1798611478749045629.crp b/maha/rexy/cache/1798611478749045629.crp
new file mode 100644
index 00000000..63ea829f
Binary files /dev/null and b/maha/rexy/cache/1798611478749045629.crp differ
diff --git a/maha/rexy/cache/1808603028645862129.crp b/maha/rexy/cache/1808603028645862129.crp
index 1ef39e7b..100cba88 100644
Binary files a/maha/rexy/cache/1808603028645862129.crp and b/maha/rexy/cache/1808603028645862129.crp differ
diff --git a/maha/rexy/cache/181929176200911629.crp b/maha/rexy/cache/181929176200911629.crp
deleted file mode 100644
index 81a8ec1c..00000000
Binary files a/maha/rexy/cache/181929176200911629.crp and /dev/null differ
diff --git a/maha/rexy/cache/1826255166542578679.crp b/maha/rexy/cache/1826255166542578679.crp
deleted file mode 100644
index 9e4fb4dc..00000000
Binary files a/maha/rexy/cache/1826255166542578679.crp and /dev/null differ
diff --git a/maha/rexy/cache/1840399521284703486.crp b/maha/rexy/cache/1840399521284703486.crp
deleted file mode 100644
index e84420b3..00000000
Binary files a/maha/rexy/cache/1840399521284703486.crp and /dev/null differ
diff --git a/maha/rexy/cache/1686482701562252090.crp b/maha/rexy/cache/1842835142405551236.crp
similarity index 53%
rename from maha/rexy/cache/1686482701562252090.crp
rename to maha/rexy/cache/1842835142405551236.crp
index eef458a8..d5421b34 100644
Binary files a/maha/rexy/cache/1686482701562252090.crp and b/maha/rexy/cache/1842835142405551236.crp differ
diff --git a/maha/rexy/cache/1848101940709675885.crp b/maha/rexy/cache/1848101940709675885.crp
index 8c214704..b4b548de 100644
Binary files a/maha/rexy/cache/1848101940709675885.crp and b/maha/rexy/cache/1848101940709675885.crp differ
diff --git a/maha/rexy/cache/185050227907123710.crp b/maha/rexy/cache/185050227907123710.crp
new file mode 100644
index 00000000..dfc4c3d3
Binary files /dev/null and b/maha/rexy/cache/185050227907123710.crp differ
diff --git a/maha/rexy/cache/1855397033615740840.crp b/maha/rexy/cache/1855397033615740840.crp
new file mode 100644
index 00000000..3e5d6026
Binary files /dev/null and b/maha/rexy/cache/1855397033615740840.crp differ
diff --git a/maha/rexy/cache/1278110300239154005.crp b/maha/rexy/cache/185797351314924955.crp
similarity index 54%
rename from maha/rexy/cache/1278110300239154005.crp
rename to maha/rexy/cache/185797351314924955.crp
index 3be0aa7f..3897efb5 100644
Binary files a/maha/rexy/cache/1278110300239154005.crp and b/maha/rexy/cache/185797351314924955.crp differ
diff --git a/maha/rexy/cache/1871441001122774078.crp b/maha/rexy/cache/1871441001122774078.crp
new file mode 100644
index 00000000..87cfc743
Binary files /dev/null and b/maha/rexy/cache/1871441001122774078.crp differ
diff --git a/maha/rexy/cache/1886234117030373784.crp b/maha/rexy/cache/1886234117030373784.crp
deleted file mode 100644
index b06d6b6f..00000000
Binary files a/maha/rexy/cache/1886234117030373784.crp and /dev/null differ
diff --git a/maha/rexy/cache/1888802017497107539.crp b/maha/rexy/cache/1888802017497107539.crp
index 4cbdcbc8..4273e2cb 100644
Binary files a/maha/rexy/cache/1888802017497107539.crp and b/maha/rexy/cache/1888802017497107539.crp differ
diff --git a/maha/rexy/cache/193985349759476522.crp b/maha/rexy/cache/193985349759476522.crp
deleted file mode 100644
index 6e9e8371..00000000
Binary files a/maha/rexy/cache/193985349759476522.crp and /dev/null differ
diff --git a/maha/rexy/cache/1973253902213396601.crp b/maha/rexy/cache/1973253902213396601.crp
new file mode 100644
index 00000000..505d4154
Binary files /dev/null and b/maha/rexy/cache/1973253902213396601.crp differ
diff --git a/maha/rexy/cache/1978478955283034966.crp b/maha/rexy/cache/1978478955283034966.crp
deleted file mode 100644
index cd5f2091..00000000
Binary files a/maha/rexy/cache/1978478955283034966.crp and /dev/null differ
diff --git a/maha/rexy/cache/1984754372936505376.crp b/maha/rexy/cache/1984754372936505376.crp
index 3da68514..9f358a3a 100644
Binary files a/maha/rexy/cache/1984754372936505376.crp and b/maha/rexy/cache/1984754372936505376.crp differ
diff --git a/maha/rexy/cache/2022313584198488055.crp b/maha/rexy/cache/2022313584198488055.crp
index 69a5ee97..412043a7 100644
Binary files a/maha/rexy/cache/2022313584198488055.crp and b/maha/rexy/cache/2022313584198488055.crp differ
diff --git a/maha/rexy/cache/2034266664387984599.crp b/maha/rexy/cache/2034266664387984599.crp
new file mode 100644
index 00000000..fd853566
Binary files /dev/null and b/maha/rexy/cache/2034266664387984599.crp differ
diff --git a/maha/rexy/cache/204764554858130421.crp b/maha/rexy/cache/204764554858130421.crp
new file mode 100644
index 00000000..e59ddd76
Binary files /dev/null and b/maha/rexy/cache/204764554858130421.crp differ
diff --git a/maha/rexy/cache/2099812429817986682.crp b/maha/rexy/cache/2099812429817986682.crp
index 0956d59c..d0f4f90e 100644
Binary files a/maha/rexy/cache/2099812429817986682.crp and b/maha/rexy/cache/2099812429817986682.crp differ
diff --git a/maha/rexy/cache/2109347316146184271.crp b/maha/rexy/cache/2109347316146184271.crp
new file mode 100644
index 00000000..5801eee9
Binary files /dev/null and b/maha/rexy/cache/2109347316146184271.crp differ
diff --git a/maha/rexy/cache/2110096684829937865.crp b/maha/rexy/cache/2110096684829937865.crp
index 58d9ea2b..3dd0a3e5 100644
Binary files a/maha/rexy/cache/2110096684829937865.crp and b/maha/rexy/cache/2110096684829937865.crp differ
diff --git a/maha/rexy/cache/2121081355711202908.crp b/maha/rexy/cache/2121081355711202908.crp
deleted file mode 100644
index 595ff2f9..00000000
Binary files a/maha/rexy/cache/2121081355711202908.crp and /dev/null differ
diff --git a/maha/rexy/cache/1167036203385316542.crp b/maha/rexy/cache/21431179100248138.crp
similarity index 56%
rename from maha/rexy/cache/1167036203385316542.crp
rename to maha/rexy/cache/21431179100248138.crp
index 2c060157..1d944ca5 100644
Binary files a/maha/rexy/cache/1167036203385316542.crp and b/maha/rexy/cache/21431179100248138.crp differ
diff --git a/maha/rexy/cache/2144522883530804765.crp b/maha/rexy/cache/2144522883530804765.crp
deleted file mode 100644
index 08127e27..00000000
Binary files a/maha/rexy/cache/2144522883530804765.crp and /dev/null differ
diff --git a/maha/rexy/cache/2185537739717480824.crp b/maha/rexy/cache/2185537739717480824.crp
new file mode 100644
index 00000000..bbb1d8d4
Binary files /dev/null and b/maha/rexy/cache/2185537739717480824.crp differ
diff --git a/maha/rexy/cache/2192927260012056888.crp b/maha/rexy/cache/2192927260012056888.crp
index d69e4d67..41948323 100644
Binary files a/maha/rexy/cache/2192927260012056888.crp and b/maha/rexy/cache/2192927260012056888.crp differ
diff --git a/maha/rexy/cache/220703359786771696.crp b/maha/rexy/cache/220703359786771696.crp
index 98028027..a92ff5bb 100644
Binary files a/maha/rexy/cache/220703359786771696.crp and b/maha/rexy/cache/220703359786771696.crp differ
diff --git a/maha/rexy/cache/2209635063912403976.crp b/maha/rexy/cache/2209635063912403976.crp
new file mode 100644
index 00000000..a3d9ba88
Binary files /dev/null and b/maha/rexy/cache/2209635063912403976.crp differ
diff --git a/maha/rexy/cache/1283494076104391807.crp b/maha/rexy/cache/2230075177055529762.crp
similarity index 73%
rename from maha/rexy/cache/1283494076104391807.crp
rename to maha/rexy/cache/2230075177055529762.crp
index dc17fe87..9dbbadaf 100644
Binary files a/maha/rexy/cache/1283494076104391807.crp and b/maha/rexy/cache/2230075177055529762.crp differ
diff --git a/maha/rexy/cache/2277241477858860334.crp b/maha/rexy/cache/2277241477858860334.crp
new file mode 100644
index 00000000..6876dc72
Binary files /dev/null and b/maha/rexy/cache/2277241477858860334.crp differ
diff --git a/maha/rexy/cache/2289334779028029166.crp b/maha/rexy/cache/2289334779028029166.crp
deleted file mode 100644
index 683e3acd..00000000
Binary files a/maha/rexy/cache/2289334779028029166.crp and /dev/null differ
diff --git a/maha/rexy/cache/2290304958734306845.crp b/maha/rexy/cache/2290304958734306845.crp
new file mode 100644
index 00000000..f575f87c
Binary files /dev/null and b/maha/rexy/cache/2290304958734306845.crp differ
diff --git a/maha/rexy/cache/254241458105254116.crp b/maha/rexy/cache/254241458105254116.crp
new file mode 100644
index 00000000..472bb298
Binary files /dev/null and b/maha/rexy/cache/254241458105254116.crp differ
diff --git a/maha/rexy/cache/264547553084591777.crp b/maha/rexy/cache/264547553084591777.crp
new file mode 100644
index 00000000..578ed04d
Binary files /dev/null and b/maha/rexy/cache/264547553084591777.crp differ
diff --git a/maha/rexy/cache/320211313456944443.crp b/maha/rexy/cache/320211313456944443.crp
index 9a7cb00b..34b80585 100644
Binary files a/maha/rexy/cache/320211313456944443.crp and b/maha/rexy/cache/320211313456944443.crp differ
diff --git a/maha/rexy/cache/320304102382400183.crp b/maha/rexy/cache/320304102382400183.crp
index 389890a7..95d9c3ec 100644
Binary files a/maha/rexy/cache/320304102382400183.crp and b/maha/rexy/cache/320304102382400183.crp differ
diff --git a/maha/rexy/cache/320473524479361592.crp b/maha/rexy/cache/320473524479361592.crp
new file mode 100644
index 00000000..8595eade
Binary files /dev/null and b/maha/rexy/cache/320473524479361592.crp differ
diff --git a/maha/rexy/cache/341640259443659970.crp b/maha/rexy/cache/341640259443659970.crp
new file mode 100644
index 00000000..bfc95c18
Binary files /dev/null and b/maha/rexy/cache/341640259443659970.crp differ
diff --git a/maha/rexy/cache/36233057712171787.crp b/maha/rexy/cache/36233057712171787.crp
new file mode 100644
index 00000000..d3ba7a8e
Binary files /dev/null and b/maha/rexy/cache/36233057712171787.crp differ
diff --git a/maha/rexy/cache/362350801306536472.crp b/maha/rexy/cache/362350801306536472.crp
new file mode 100644
index 00000000..646b126a
Binary files /dev/null and b/maha/rexy/cache/362350801306536472.crp differ
diff --git a/maha/rexy/cache/406482220905023619.crp b/maha/rexy/cache/406482220905023619.crp
deleted file mode 100644
index 9ae0b7f3..00000000
Binary files a/maha/rexy/cache/406482220905023619.crp and /dev/null differ
diff --git a/maha/rexy/cache/409784257943035169.crp b/maha/rexy/cache/409784257943035169.crp
deleted file mode 100644
index 1d84fd0b..00000000
Binary files a/maha/rexy/cache/409784257943035169.crp and /dev/null differ
diff --git a/maha/rexy/cache/444280113159262116.crp b/maha/rexy/cache/444280113159262116.crp
deleted file mode 100644
index 5a39ed69..00000000
Binary files a/maha/rexy/cache/444280113159262116.crp and /dev/null differ
diff --git a/maha/rexy/cache/461824166411444876.crp b/maha/rexy/cache/461824166411444876.crp
new file mode 100644
index 00000000..c8f07b4d
Binary files /dev/null and b/maha/rexy/cache/461824166411444876.crp differ
diff --git a/maha/rexy/cache/469780842676988165.crp b/maha/rexy/cache/469780842676988165.crp
index 956cf7b5..e55de0a6 100644
Binary files a/maha/rexy/cache/469780842676988165.crp and b/maha/rexy/cache/469780842676988165.crp differ
diff --git a/maha/rexy/cache/475799279787834544.crp b/maha/rexy/cache/475799279787834544.crp
deleted file mode 100644
index c2509877..00000000
Binary files a/maha/rexy/cache/475799279787834544.crp and /dev/null differ
diff --git a/maha/rexy/cache/479276905484415988.crp b/maha/rexy/cache/479276905484415988.crp
new file mode 100644
index 00000000..5e076d09
Binary files /dev/null and b/maha/rexy/cache/479276905484415988.crp differ
diff --git a/maha/rexy/cache/48497649900183094.crp b/maha/rexy/cache/48497649900183094.crp
index 5bbd66e4..e36fef84 100644
Binary files a/maha/rexy/cache/48497649900183094.crp and b/maha/rexy/cache/48497649900183094.crp differ
diff --git a/maha/rexy/cache/491330973141900804.crp b/maha/rexy/cache/491330973141900804.crp
index 6b0ac2f9..bfec359f 100644
Binary files a/maha/rexy/cache/491330973141900804.crp and b/maha/rexy/cache/491330973141900804.crp differ
diff --git a/maha/rexy/cache/572338029879256695.crp b/maha/rexy/cache/572338029879256695.crp
new file mode 100644
index 00000000..55ec0671
Binary files /dev/null and b/maha/rexy/cache/572338029879256695.crp differ
diff --git a/maha/rexy/cache/584949730565938681.crp b/maha/rexy/cache/584949730565938681.crp
new file mode 100644
index 00000000..0f8b88d9
Binary files /dev/null and b/maha/rexy/cache/584949730565938681.crp differ
diff --git a/maha/rexy/cache/630540614238142002.crp b/maha/rexy/cache/630540614238142002.crp
index 862e2344..60066315 100644
Binary files a/maha/rexy/cache/630540614238142002.crp and b/maha/rexy/cache/630540614238142002.crp differ
diff --git a/maha/rexy/cache/2277793059091916889.crp b/maha/rexy/cache/648061749748262946.crp
similarity index 65%
rename from maha/rexy/cache/2277793059091916889.crp
rename to maha/rexy/cache/648061749748262946.crp
index a235599f..e06bfae3 100644
Binary files a/maha/rexy/cache/2277793059091916889.crp and b/maha/rexy/cache/648061749748262946.crp differ
diff --git a/maha/rexy/cache/680953300077283277.crp b/maha/rexy/cache/680953300077283277.crp
index a91cac68..5e6ff752 100644
Binary files a/maha/rexy/cache/680953300077283277.crp and b/maha/rexy/cache/680953300077283277.crp differ
diff --git a/maha/rexy/cache/697667471121152215.crp b/maha/rexy/cache/697667471121152215.crp
deleted file mode 100644
index 8293a1f7..00000000
Binary files a/maha/rexy/cache/697667471121152215.crp and /dev/null differ
diff --git a/maha/rexy/cache/761775007615295969.crp b/maha/rexy/cache/761775007615295969.crp
new file mode 100644
index 00000000..a1e54c62
Binary files /dev/null and b/maha/rexy/cache/761775007615295969.crp differ
diff --git a/maha/rexy/cache/762512842798827136.crp b/maha/rexy/cache/762512842798827136.crp
index 7cd527fc..1a8dd918 100644
Binary files a/maha/rexy/cache/762512842798827136.crp and b/maha/rexy/cache/762512842798827136.crp differ
diff --git a/maha/rexy/cache/1368145440277422482.crp b/maha/rexy/cache/780637979322469641.crp
similarity index 57%
rename from maha/rexy/cache/1368145440277422482.crp
rename to maha/rexy/cache/780637979322469641.crp
index f5548cd6..0b2c5c8b 100644
Binary files a/maha/rexy/cache/1368145440277422482.crp and b/maha/rexy/cache/780637979322469641.crp differ
diff --git a/maha/rexy/cache/793817324574194762.crp b/maha/rexy/cache/793817324574194762.crp
index abaf2b96..04f5d4bf 100644
Binary files a/maha/rexy/cache/793817324574194762.crp and b/maha/rexy/cache/793817324574194762.crp differ
diff --git a/maha/rexy/cache/808887295847442840.crp b/maha/rexy/cache/808887295847442840.crp
new file mode 100644
index 00000000..1460f602
Binary files /dev/null and b/maha/rexy/cache/808887295847442840.crp differ
diff --git a/maha/rexy/cache/817551295329783487.crp b/maha/rexy/cache/817551295329783487.crp
deleted file mode 100644
index 8befd152..00000000
Binary files a/maha/rexy/cache/817551295329783487.crp and /dev/null differ
diff --git a/maha/rexy/cache/824588850509660474.crp b/maha/rexy/cache/824588850509660474.crp
index 043163cc..13ae62b6 100644
Binary files a/maha/rexy/cache/824588850509660474.crp and b/maha/rexy/cache/824588850509660474.crp differ
diff --git a/maha/rexy/cache/829273972363410216.crp b/maha/rexy/cache/829273972363410216.crp
index 543617bd..c88eead6 100644
Binary files a/maha/rexy/cache/829273972363410216.crp and b/maha/rexy/cache/829273972363410216.crp differ
diff --git a/maha/rexy/cache/860664193158545569.crp b/maha/rexy/cache/860664193158545569.crp
deleted file mode 100644
index 797fbb8c..00000000
Binary files a/maha/rexy/cache/860664193158545569.crp and /dev/null differ
diff --git a/maha/rexy/cache/874598931234702037.crp b/maha/rexy/cache/874598931234702037.crp
deleted file mode 100644
index 0d4464ef..00000000
Binary files a/maha/rexy/cache/874598931234702037.crp and /dev/null differ
diff --git a/maha/rexy/cache/886415422171591197.crp b/maha/rexy/cache/886415422171591197.crp
new file mode 100644
index 00000000..8a6e6373
Binary files /dev/null and b/maha/rexy/cache/886415422171591197.crp differ
diff --git a/maha/rexy/cache/1411810044364251560.crp b/maha/rexy/cache/888318901751378703.crp
similarity index 50%
rename from maha/rexy/cache/1411810044364251560.crp
rename to maha/rexy/cache/888318901751378703.crp
index 21108870..7997f91e 100644
Binary files a/maha/rexy/cache/1411810044364251560.crp and b/maha/rexy/cache/888318901751378703.crp differ
diff --git a/maha/rexy/cache/890516925238349982.crp b/maha/rexy/cache/890516925238349982.crp
deleted file mode 100644
index ac3c3895..00000000
Binary files a/maha/rexy/cache/890516925238349982.crp and /dev/null differ
diff --git a/maha/rexy/cache/907269771251339198.crp b/maha/rexy/cache/907269771251339198.crp
deleted file mode 100644
index 723a2221..00000000
Binary files a/maha/rexy/cache/907269771251339198.crp and /dev/null differ


Test patch:
diff --git a/tests/parsers/test_numeral.py b/tests/parsers/test_numeral.py
index 7d2be842..dfe2298b 100644
--- a/tests/parsers/test_numeral.py
+++ b/tests/parsers/test_numeral.py
@@ -354,6 +354,8 @@ def test_numbers(input, expected):
         (10.5, "10 فاصله 5"),
         (33.3, "ثلاثة وثلاثين فاصلة ثلاثة"),
         (100 / 3, "ثلث مية"),
+        (1025, "الف وربع مية"),
+        (2070, "الفين وعشرين ونص مية"),
     ],
 )
 def test_fasila_numbers(input, expected):
@@ -421,6 +423,23 @@ def test_combinations(input, expected):
     assert_expression_output(parse_dimension(input, numeral=True), expected)
 
 
+@pytest.mark.parametrize(
+    "expected, input",
+    [
+        (100_000, "مئة الف"),
+        (100_000_000_000, "مئة الف مليون"),
+        (2_000_000, "عشرين مئة الف"),
+        (300_000, "ثلاثمئة الف"),
+        (200_500_000, "مئتين مليون وخمس مئة الف"),
+        (4_000_000_000_000, "اربع الاف مليار"),
+        (2_100_000.100_01, "واحد وعشرين مئة الف فاصلة مئة الف وعشرة"),
+        (3_300_000, "ثلاثة وثلاثين مئة الف"),
+    ],
+)
+def test_hierarchical_parsing(input, expected):
+    assert_expression_output(parse_dimension(input, numeral=True), expected)
+
+
 @pytest.mark.parametrize(
     "input",
     [