Skip to content

Commit d5f3f54

Browse files
committed
SNOW-2346552: add more edge ases
1 parent 84bef45 commit d5f3f54

2 files changed

Lines changed: 253 additions & 23 deletions

File tree

src/snowflake/snowpark/_internal/type_utils.py

Lines changed: 93 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,9 +1465,27 @@ def format_day_time_interval_for_display(
14651465
Formatted interval string (e.g., "INTERVAL '01:30:45' HOUR TO SECOND")
14661466
"""
14671467
if isinstance(cell, datetime.timedelta):
1468-
total_seconds_float = cell.total_seconds()
1468+
# Heuristic: Use Decimal for extreme values near 64-bit boundary, float for normal values
1469+
total_seconds_approx = cell.total_seconds()
1470+
1471+
# Check if we're approaching values where float precision becomes problematic
1472+
# Be conservative: use Decimal for large values to ensure precision
1473+
# This corresponds to roughly 3 million years - normal use cases are well below this
1474+
if (
1475+
abs(total_seconds_approx) > 1e11
1476+
): # ~100 gigaseconds, very conservative threshold
1477+
# Use Decimal arithmetic for precise conversion to avoid floating-point precision loss
1478+
total_seconds_value = (
1479+
decimal.Decimal(cell.days) * decimal.Decimal(86400)
1480+
+ decimal.Decimal(cell.seconds)
1481+
+ decimal.Decimal(cell.microseconds) / decimal.Decimal(1_000_000)
1482+
)
1483+
else:
1484+
# Use fast float path for normal values
1485+
total_seconds_value = cell.total_seconds()
1486+
14691487
interval_str = format_day_time_interval(
1470-
total_seconds_float, start_field, end_field
1488+
total_seconds_value, start_field, end_field
14711489
)
14721490
elif isinstance(cell, str):
14731491
# Raw string that needs to be formatted (e.g., "1 01:01:01.7878")
@@ -1492,35 +1510,96 @@ def format_day_time_interval_for_display(
14921510

14931511

14941512
def format_day_time_interval(
1495-
total_seconds_float: float, start_field: int, end_field: int
1513+
total_seconds_value: Union[float, decimal.Decimal], start_field: int, end_field: int
14961514
) -> str:
14971515
"""
14981516
Format a DayTimeIntervalType value for display in _show_string_spark().
14991517
15001518
Args:
1501-
total_seconds_float: Total seconds as a float (can be negative)
1519+
total_seconds_value: Total seconds as either float or Decimal (can be negative)
15021520
start_field: Start field constant from DayTimeIntervalType (DAY=0, HOUR=1, MINUTE=2, SECOND=3)
15031521
end_field: End field constant from DayTimeIntervalType (DAY=0, HOUR=1, MINUTE=2, SECOND=3)
15041522
15051523
Returns:
15061524
Formatted interval string (e.g., "01:30:45", "2 12:30", "05", etc.)
15071525
"""
1508-
is_negative = total_seconds_float < 0
1509-
abs_total_seconds = abs(total_seconds_float)
1526+
is_negative = total_seconds_value < 0
1527+
abs_total_seconds = abs(total_seconds_value)
1528+
1529+
# Determine if we're working with Decimal for high-precision arithmetic
1530+
use_decimal = isinstance(total_seconds_value, decimal.Decimal)
15101531

15111532
days = int(abs_total_seconds) // 86400
15121533
remaining_seconds = abs_total_seconds - (days * 86400)
15131534
hours = int(remaining_seconds) // 3600
15141535
remaining_after_hours = remaining_seconds - (hours * 3600)
15151536
minutes = int(remaining_after_hours) // 60
1516-
seconds = remaining_after_hours - (minutes * 60)
1537+
1538+
# Calculate seconds more precisely to avoid floating-point accumulation errors
1539+
# Use the original total and subtract the calculated day/hour/minute components
1540+
if use_decimal:
1541+
total_non_second_time = (
1542+
decimal.Decimal(days * 86400)
1543+
+ decimal.Decimal(hours * 3600)
1544+
+ decimal.Decimal(minutes * 60)
1545+
)
1546+
else:
1547+
total_non_second_time = (days * 86400) + (hours * 3600) + (minutes * 60)
1548+
seconds = abs_total_seconds - total_non_second_time
15171549

15181550
sign = "-" if is_negative else ""
15191551

15201552
def format_with_leading_zero(value: int) -> str:
15211553
"""Format integer with leading zero if < 10, otherwise as-is"""
15221554
return f"{value:02d}" if value < 10 else f"{value}"
15231555

1556+
def format_seconds_with_precision(
1557+
seconds_value: Union[float, decimal.Decimal]
1558+
) -> str:
1559+
"""Format seconds with full precision, preserving trailing zeros for proper padding"""
1560+
if isinstance(seconds_value, decimal.Decimal):
1561+
# Use Decimal precision formatting
1562+
if seconds_value == int(seconds_value):
1563+
return f"{int(seconds_value):02d}"
1564+
else:
1565+
# For fractional seconds, ensure proper leading zero padding
1566+
integer_part = int(seconds_value)
1567+
if integer_part < 10:
1568+
# Format with leading zero for the integer part
1569+
formatted = f"{seconds_value:.6f}".rstrip("0")
1570+
if formatted.endswith("."):
1571+
return f"{integer_part:02d}"
1572+
# Replace the integer part with zero-padded version
1573+
decimal_part = formatted.split(".", 1)[1]
1574+
return f"{integer_part:02d}.{decimal_part}"
1575+
else:
1576+
# For >= 10, use normal formatting
1577+
formatted = f"{seconds_value:.6f}".rstrip("0")
1578+
if formatted.endswith("."):
1579+
return f"{integer_part}"
1580+
return formatted
1581+
else:
1582+
# Float precision formatting (original logic)
1583+
if seconds_value == int(seconds_value):
1584+
return f"{int(seconds_value):02d}"
1585+
else:
1586+
# For fractional seconds, ensure proper leading zero padding
1587+
integer_part = int(seconds_value)
1588+
if integer_part < 10:
1589+
# Format with leading zero for the integer part
1590+
formatted = f"{seconds_value:.6f}".rstrip("0")
1591+
if formatted.endswith("."):
1592+
return f"{integer_part:02d}"
1593+
# Replace the integer part with zero-padded version
1594+
decimal_part = formatted.split(".", 1)[1]
1595+
return f"{integer_part:02d}.{decimal_part}"
1596+
else:
1597+
# For >= 10, use normal formatting
1598+
formatted = f"{seconds_value:.6f}".rstrip("0")
1599+
if formatted.endswith("."):
1600+
return f"{integer_part}"
1601+
return formatted
1602+
15241603
# For single field intervals, extract just that component
15251604
if start_field == end_field:
15261605
if start_field == DayTimeIntervalType.DAY:
@@ -1537,19 +1616,8 @@ def format_with_leading_zero(value: int) -> str:
15371616
total_secs_int = int(abs_total_seconds)
15381617
return f"{sign}{format_with_leading_zero(total_secs_int)}"
15391618
else:
1540-
# For fractional seconds, format with leading zero if < 10
1541-
if abs_total_seconds < 10:
1542-
# Format with leading zero: split into integer and fractional parts
1543-
integer_part = int(abs_total_seconds)
1544-
fractional_part = abs_total_seconds - integer_part
1545-
if fractional_part == 0:
1546-
return f"{sign}{integer_part:02d}"
1547-
else:
1548-
# Format fractional part and remove leading '0.'
1549-
frac_str = f"{fractional_part:.6f}"[2:].rstrip("0")
1550-
return f"{sign}{integer_part:02d}.{frac_str}"
1551-
else:
1552-
return f"{sign}{abs_total_seconds:g}"
1619+
# Use unified formatting that handles both float and Decimal
1620+
return f"{sign}{format_seconds_with_precision(abs_total_seconds)}"
15531621

15541622
# For multi-field intervals, format based on start/end fields
15551623
if start_field == DayTimeIntervalType.DAY:
@@ -1566,7 +1634,7 @@ def format_with_leading_zero(value: int) -> str:
15661634
if seconds == int(seconds):
15671635
return f"{sign}{days} {hours_str}:{minutes:02d}:{int(seconds):02d}"
15681636
else:
1569-
return f"{sign}{days} {hours_str}:{minutes:02d}:{seconds:06.3f}"
1637+
return f"{sign}{days} {hours_str}:{minutes:02d}:{format_seconds_with_precision(seconds)}"
15701638
elif start_field == DayTimeIntervalType.HOUR:
15711639
# HOUR TO X format: "HH:MM:SS" (no days)
15721640
total_hours = int(abs_total_seconds) // 3600
@@ -1580,7 +1648,7 @@ def format_with_leading_zero(value: int) -> str:
15801648
if secs == int(secs):
15811649
return f"{sign}{format_with_leading_zero(total_hours)}:{mins:02d}:{int(secs):02d}"
15821650
else:
1583-
return f"{sign}{format_with_leading_zero(total_hours)}:{mins:02d}:{secs:06.3f}"
1651+
return f"{sign}{format_with_leading_zero(total_hours)}:{mins:02d}:{format_seconds_with_precision(secs)}"
15841652
elif start_field == DayTimeIntervalType.MINUTE:
15851653
# MINUTE TO X format: "MM:SS" (no days or hours)
15861654
total_minutes = int(abs_total_seconds) // 60
@@ -1590,7 +1658,9 @@ def format_with_leading_zero(value: int) -> str:
15901658
if remaining_secs == int(remaining_secs):
15911659
return f"{sign}{minutes_str}:{int(remaining_secs):02d}"
15921660
else:
1593-
return f"{sign}{minutes_str}:{remaining_secs:06.3f}"
1661+
return (
1662+
f"{sign}{minutes_str}:{format_seconds_with_precision(remaining_secs)}"
1663+
)
15941664

15951665

15961666
# Type hints

tests/integ/test_dataframe.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3213,6 +3213,166 @@ def test_show_interval_formatting(session):
32133213
"""
32143214
)
32153215

3216+
# === Edge Cases for Decimal Precision and Large Values ===
3217+
3218+
# Large positive DAY TO HOUR intervals
3219+
df = session.sql("SELECT INTERVAL '106751991 04' DAY TO HOUR as large_day_to_hour")
3220+
assert df._show_string_spark(truncate=False) == dedent(
3221+
"""\
3222+
+-----------------------------------+
3223+
|"LARGE_DAY_TO_HOUR" |
3224+
+-----------------------------------+
3225+
|INTERVAL '106751991 04' DAY TO HOUR|
3226+
+-----------------------------------+
3227+
"""
3228+
)
3229+
3230+
# Large positive DAY TO MINUTE intervals
3231+
df = session.sql(
3232+
"SELECT INTERVAL '106751991 04:00' DAY TO MINUTE as large_day_to_minute"
3233+
)
3234+
assert df._show_string_spark(truncate=False) == dedent(
3235+
"""\
3236+
+----------------------------------------+
3237+
|"LARGE_DAY_TO_MINUTE" |
3238+
+----------------------------------------+
3239+
|INTERVAL '106751991 04:00' DAY TO MINUTE|
3240+
+----------------------------------------+
3241+
"""
3242+
)
3243+
3244+
# Large positive DAY TO SECOND intervals with high precision fractional seconds
3245+
df = session.sql(
3246+
"SELECT INTERVAL '106751991 04:00:54.775807' DAY TO SECOND as large_day_to_second"
3247+
)
3248+
assert df._show_string_spark(truncate=False) == dedent(
3249+
"""\
3250+
+--------------------------------------------------+
3251+
|"LARGE_DAY_TO_SECOND" |
3252+
+--------------------------------------------------+
3253+
|INTERVAL '106751991 04:00:54.775807' DAY TO SECOND|
3254+
+--------------------------------------------------+
3255+
"""
3256+
)
3257+
3258+
# Large negative DAY TO HOUR intervals
3259+
df = session.sql(
3260+
"SELECT INTERVAL '-106751991 04' DAY TO HOUR as large_negative_day_to_hour"
3261+
)
3262+
assert df._show_string_spark(truncate=False) == dedent(
3263+
"""\
3264+
+------------------------------------+
3265+
|"LARGE_NEGATIVE_DAY_TO_HOUR" |
3266+
+------------------------------------+
3267+
|INTERVAL '-106751991 04' DAY TO HOUR|
3268+
+------------------------------------+
3269+
"""
3270+
)
3271+
3272+
# Large negative DAY TO MINUTE intervals
3273+
df = session.sql(
3274+
"SELECT INTERVAL '-106751991 04:00' DAY TO MINUTE as large_negative_day_to_minute"
3275+
)
3276+
assert df._show_string_spark(truncate=False) == dedent(
3277+
"""\
3278+
+-----------------------------------------+
3279+
|"LARGE_NEGATIVE_DAY_TO_MINUTE" |
3280+
+-----------------------------------------+
3281+
|INTERVAL '-106751991 04:00' DAY TO MINUTE|
3282+
+-----------------------------------------+
3283+
"""
3284+
)
3285+
3286+
# Large negative DAY TO SECOND intervals with high precision fractional seconds
3287+
df = session.sql(
3288+
"SELECT INTERVAL '-106751991 04:00:54.775808' DAY TO SECOND as large_negative_day_to_second"
3289+
)
3290+
assert df._show_string_spark(truncate=False) == dedent(
3291+
"""\
3292+
+---------------------------------------------------+
3293+
|"LARGE_NEGATIVE_DAY_TO_SECOND" |
3294+
+---------------------------------------------------+
3295+
|INTERVAL '-106751991 04:00:54.775808' DAY TO SECOND|
3296+
+---------------------------------------------------+
3297+
"""
3298+
)
3299+
3300+
# Extremely large positive YEAR intervals
3301+
df = session.sql("SELECT INTERVAL '178956970' YEAR as extremely_large_year")
3302+
assert df._show_string_spark(truncate=False) == dedent(
3303+
"""\
3304+
+-------------------------+
3305+
|"EXTREMELY_LARGE_YEAR" |
3306+
+-------------------------+
3307+
|INTERVAL '178956970' YEAR|
3308+
+-------------------------+
3309+
"""
3310+
)
3311+
3312+
# Extremely large negative YEAR intervals
3313+
df = session.sql(
3314+
"SELECT INTERVAL '-178956970' YEAR as extremely_large_negative_year"
3315+
)
3316+
assert df._show_string_spark(truncate=False) == dedent(
3317+
"""\
3318+
+-------------------------------+
3319+
|"EXTREMELY_LARGE_NEGATIVE_YEAR"|
3320+
+-------------------------------+
3321+
|INTERVAL '-178956970' YEAR |
3322+
+-------------------------------+
3323+
"""
3324+
)
3325+
3326+
# Large positive DAY intervals
3327+
df = session.sql("SELECT INTERVAL '106751991' DAY as extremely_large_day")
3328+
assert df._show_string_spark(truncate=False) == dedent(
3329+
"""\
3330+
+------------------------+
3331+
|"EXTREMELY_LARGE_DAY" |
3332+
+------------------------+
3333+
|INTERVAL '106751991' DAY|
3334+
+------------------------+
3335+
"""
3336+
)
3337+
3338+
# Large negative DAY intervals
3339+
df = session.sql("SELECT INTERVAL '-106751991' DAY as extremely_large_negative_day")
3340+
assert df._show_string_spark(truncate=False) == dedent(
3341+
"""\
3342+
+------------------------------+
3343+
|"EXTREMELY_LARGE_NEGATIVE_DAY"|
3344+
+------------------------------+
3345+
|INTERVAL '-106751991' DAY |
3346+
+------------------------------+
3347+
"""
3348+
)
3349+
3350+
# High precision positive fractional SECOND intervals
3351+
df = session.sql("SELECT INTERVAL '54.775807' SECOND as high_precision_second")
3352+
assert df._show_string_spark(truncate=False) == dedent(
3353+
"""\
3354+
+---------------------------+
3355+
|"HIGH_PRECISION_SECOND" |
3356+
+---------------------------+
3357+
|INTERVAL '54.775807' SECOND|
3358+
+---------------------------+
3359+
"""
3360+
)
3361+
3362+
# High precision negative fractional SECOND intervals
3363+
df = session.sql(
3364+
"SELECT INTERVAL '-54.775807' SECOND as high_precision_negative_second"
3365+
)
3366+
assert df._show_string_spark(truncate=False) == dedent(
3367+
"""\
3368+
+--------------------------------+
3369+
|"HIGH_PRECISION_NEGATIVE_SECOND"|
3370+
+--------------------------------+
3371+
|INTERVAL '-54.775807' SECOND |
3372+
+--------------------------------+
3373+
"""
3374+
)
3375+
32163376

32173377
@pytest.mark.parametrize("data", [[0, 1, 2, 3], ["", "a"], [False, True], [None]])
32183378
def test_create_dataframe_with_single_value(session, data):

0 commit comments

Comments
 (0)