Source code for fabrictools.powerquery.number

"""Power Query ``Number.*`` column expressions."""

from __future__ import annotations

from typing import Union

from pyspark.sql import Column
from pyspark.sql import functions as F

from fabrictools.powerquery.text import Text

__all__ = ["Number"]


def _from_text_expr(expr: Union[Column, str]) -> Column:
    """Internal: parse text to nullable double (``fxToNumber`` rules)."""
    t0 = Text.From(expr)
    t1 = Text.Select(t0, list("0123456789,.-"))

    empty = (t1 == F.lit("")) | (t1 == F.lit("-"))
    has_comma = t1.contains(",")
    has_dot = t1.contains(".")
    comma_count = F.length(t1) - F.length(F.regexp_replace(t1, ",", ""))

    us_thousands = (has_comma & has_dot) | (comma_count > F.lit(1))
    us_stripped = F.regexp_replace(t1, ",", "")
    fr_normalized = F.regexp_replace(t1, ",", ".")

    parsed = (
        F.when(empty, F.lit(None).cast("double"))
        .when(us_thousands, us_stripped.cast("double"))
        .when(has_comma, fr_normalized.cast("double"))
        .when(has_dot, t1.cast("double"))
        .otherwise(t1.cast("double"))
    )
    return parsed


[docs] class Number: """Namespace for Power Query ``Number.*`` functions."""
[docs] @staticmethod def FromText(expr: Union[Column, str]) -> Column: """Convert text to a nullable number with US/FR decimal rules (Power Query ``Number.FromText``). Implements the invoicing script ``fxToNumber`` logic: strips non-numeric characters, detects thousand separators vs decimal comma/point, and parses accordingly. :param expr: Spark column or string literal. :type expr: ~pyspark.sql.Column | str :returns: Nullable ``double`` column expression. :rtype: ~pyspark.sql.Column .. rubric:: Example >>> from fabrictools import Table, Number # doctest: +SKIP >>> df = Table.TransformColumns(df, [ # doctest: +SKIP ... ("Total Invoice amount without VAT", Number.FromText), ... ]) """ return _from_text_expr(expr)
[docs] @staticmethod def ToText(expr: Union[Column, str, int, float], format: str | None = None) -> Column: """Convert a number to text (Power Query ``Number.ToText``). When ``format`` is a zero-padded mask such as ``"0000"`` or ``"00"``, the value is left-padded with zeros to that width (e.g. month ``1`` → ``"01"``). Null input stays null. :param expr: Numeric column, string column, or numeric literal. :param format: Optional custom format (``"0"`` digits = pad width). :type expr: ~pyspark.sql.Column | str | int | float :type format: str | None :returns: String column expression. :rtype: ~pyspark.sql.Column .. rubric:: Example >>> from fabrictools import Number, Date # doctest: +SKIP >>> from pyspark.sql import functions as F # doctest: +SKIP >>> Number.ToText(Date.Month(F.col("Date contractuelle")), "00") # doctest: +SKIP """ if isinstance(expr, (int, float)): col = F.lit(expr) elif isinstance(expr, str): col = F.lit(expr) else: col = expr if format is None: return col.cast("string") mask = str(format) if mask and set(mask) <= {"0"}: return F.lpad(col.cast("long").cast("string"), len(mask), "0") return col.cast("string")