From 232ed74a4be2806bda886739a20a703b9c1043ea Mon Sep 17 00:00:00 2001 From: Perry Kundert Date: Tue, 8 Oct 2024 12:49:37 -0600 Subject: [PATCH] Begin trying to simplify _isnumber --- README.md | 14 +++++++++++++ tabulate/__init__.py | 48 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 70b6839..f3d0fa9 100644 --- a/README.md +++ b/README.md @@ -813,6 +813,20 @@ data in the column is used to infer the type: ``` +The deduced type (eg. str, float) influences the rendering of any types +that have alternative representations. For example, since `Fraction` has +methods `__str__` and `__float__` defined (and hence is convertible to a +`float` and also has a `str` representation), the appropriate +representation is selected for the column's deduced type. In order to not +lose precision accidentally, types having both an `__int__` and +`__float__` represention will be considered a `float`. + +Therefore, if your table contains types convertible to int/float but you'd +*prefer* they be represented as strings, or your strings *might* all look +like numbers such as "1e23": either convert them to the desired +representation before you `tabulate`, or ensure that the column always +contains at least one other `str`. + ### Text formatting By default, `tabulate` removes leading and trailing whitespace from text diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 0d249ac..b15d7f0 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -876,25 +876,55 @@ def _isconvertible(conv, string): def _isnumber(string): - """ + """Detects if something *could* be considered a numeric value, vs. just a string. + + This promotes types convertible to both int and float to be considered + a float. Note that, iff *all* values appear to be some form of numeric + value such as eg. "1e2", they would be considered numbers! + + The exception is things that appear to be numbers but overflow to + +/-inf, eg. "1e23456"; we'll have to exclude them explicitly. + + >>> _isnumber(123) + True + >>> _isnumber(123.45) + True >>> _isnumber("123.45") True >>> _isnumber("123") True >>> _isnumber("spam") False - >>> _isnumber("123e45678") + >>> _isnumber("123e45") + True + >>> _isnumber("123e45678") # evaluates equal to 'inf', but ... isn't False >>> _isnumber("inf") True + >>> from fractions import Fraction + >>> _isnumber(Fraction(1,3)) + True + """ - if not _isconvertible(float, string): - return False - elif isinstance(string, (str, bytes)) and ( - math.isinf(float(string)) or math.isnan(float(string)) - ): - return string.lower() in ["inf", "-inf", "nan"] - return True + return ( + # fast path + type(string) in (float, int) + # covers 'NaN', +/- 'inf', and eg. '1e2', as well as any type + # convertible to int/float. + or ( + _isconvertible(float, string) + and ( + # some other type convertible to float + not isinstance(string, (str, bytes)) + # or, a numeric string eg. "1e1...", "NaN", ..., but isn't + # just an over/underflow + or ( + not (math.isinf(float(string)) or math.isnan(float(string))) + or string.lower() in ["inf", "-inf", "nan"] + ) + ) + ) + ) def _isint(string, inttype=int):