Skip to content

Commit 54f704b

Browse files
authored
fix: adjust regex check for timezone offsets, allowing : to be included (#181)
1 parent 986a03b commit 54f704b

4 files changed

Lines changed: 43 additions & 9 deletions

File tree

‎src/dve/core_engine/backends/implementations/spark/spark_helpers.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def __post_init__(self):
112112
"%H": "HH",
113113
"%M": "mm",
114114
"%S": "ss",
115-
"%z": "XX",
115+
"%z": "XXX",
116116
"%Z": "z",
117117
}
118118
"""A mapping of python to java datetime component formats. Not exhaustive but will hopefully support

‎src/dve/core_engine/backends/utilities.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"H": r"[0-9]{2}",
4141
"M": r"[0-9]{2}",
4242
"S": r"[0-9]{2}",
43-
"z": r"(\+|\-)?[0-9]+(\.[0-9]*)?",
43+
"z": r"[+-][0-9]+(:[0-9]{2})?(\.[0-9]*)?",
4444
"Z": r"[A-Z]{0,3}",
4545
}
4646

@@ -54,7 +54,6 @@ def datetime_format_to_regex(format_str: str) -> str:
5454
Helper function to convert python datetime formats to regexp string checks for casting
5555
purposes.
5656
"""
57-
5857
tokens = list(format_str.replace("%", ""))
5958

6059
for idx, tkn in enumerate(tokens):

‎tests/test_core_engine/test_backends/test_implementations/test_duckdb/test_duckdb_helpers.py‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
)
2727
from dve.core_engine.backends.utilities import datetime_format_to_regex
2828
from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
29-
from dve.metadata_parser.domain_types import formatteddatetime
29+
from dve.metadata_parser.domain_types import conformatteddate, formatteddatetime
3030

3131
@pytest.fixture
3232
def casting_test_table(temp_ddb_conn):
@@ -225,8 +225,8 @@ def test_duckdb_rel_to_dictionaries(temp_ddb_conn: DuckDBPyConnection,
225225
("date_test", datetime.date,"TRY_CAST(CASE WHEN REGEXP_FULL_MATCH(TRIM(\"date_test\"), '^[0-9]{4}\-[0-9]{2}\-[0-9]{2}$') THEN TRY_STRPTIME(TRIM(\"date_test\"), '%Y-%m-%d') ELSE NULL END as DATE)"),
226226
("timestamp_test", datetime.datetime, "TRY_CAST(CASE WHEN REGEXP_FULL_MATCH(TRIM(\"timestamp_test\"), '^[0-9]{4}\-[0-9]{2}\-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}$') THEN TRY_STRPTIME(TRIM(\"timestamp_test\"), '%Y-%m-%dT%H:%M:%S') ELSE NULL END as TIMESTAMP)"),
227227
("list_int_field", list[int], "TRY_CAST(LIST_TRANSFORM(\"list_int_field\", x -> TRIM(\"x\")) as BIGINT[])"),
228-
("basic_model", BasicModel, "TRY_CAST(STRUCT_PACK(\"str_field\":= TRIM(\"basic_model\".str_field),\"date_field\":= TRY_CAST(CASE WHEN REGEXP_FULL_MATCH(TRIM(\"basic_model\".date_field), '^[0-9]{4}\-[0-9]{2}\-[0-9]{2}$') THEN TRY_STRPTIME(TRIM(\"basic_model\".date_field), '%Y-%m-%d') ELSE NULL END as DATE),\"timestamp_field\":= TRY_CAST(CASE WHEN REGEXP_FULL_MATCH(TRIM(\"basic_model\".timestamp_field), '^[0-9]{2}/[0-9]{2}/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}(\+|\-)?[0-9]+(\.[0-9]*)?$') THEN TRY_STRPTIME(TRIM(\"basic_model\".timestamp_field), '%d/%m/%Y %H:%M:%S%z') ELSE NULL END as TIMESTAMP)) as STRUCT(str_field VARCHAR, date_field DATE, timestamp_field TIMESTAMP))"),
229-
("another_model", AnotherModel, "TRY_CAST(STRUCT_PACK(\"unique_id\":= TRIM(\"another_model\".unique_id),\"basic_models\":= LIST_TRANSFORM(\"another_model\".basic_models, x -> STRUCT_PACK(\"str_field\":= TRIM(\"x\".str_field),\"date_field\":= TRY_CAST(CASE WHEN REGEXP_FULL_MATCH(TRIM(\"x\".date_field), '^[0-9]{4}\-[0-9]{2}\-[0-9]{2}$') THEN TRY_STRPTIME(TRIM(\"x\".date_field), '%Y-%m-%d') ELSE NULL END as DATE),\"timestamp_field\":= TRY_CAST(CASE WHEN REGEXP_FULL_MATCH(TRIM(\"x\".timestamp_field), '^[0-9]{2}/[0-9]{2}/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}(\+|\-)?[0-9]+(\.[0-9]*)?$') THEN TRY_STRPTIME(TRIM(\"x\".timestamp_field), '%d/%m/%Y %H:%M:%S%z') ELSE NULL END as TIMESTAMP)))) as STRUCT(unique_id BIGINT, basic_models STRUCT(str_field VARCHAR, date_field DATE, timestamp_field TIMESTAMP)[]))")])
228+
("basic_model", BasicModel, "TRY_CAST(STRUCT_PACK(\"str_field\":= TRIM(\"basic_model\".str_field),\"date_field\":= TRY_CAST(CASE WHEN REGEXP_FULL_MATCH(TRIM(\"basic_model\".date_field), '^[0-9]{4}\-[0-9]{2}\-[0-9]{2}$') THEN TRY_STRPTIME(TRIM(\"basic_model\".date_field), '%Y-%m-%d') ELSE NULL END as DATE),\"timestamp_field\":= TRY_CAST(CASE WHEN REGEXP_FULL_MATCH(TRIM(\"basic_model\".timestamp_field), '^[0-9]{2}/[0-9]{2}/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]+(:[0-9]{2})?(\.[0-9]*)?$') THEN TRY_STRPTIME(TRIM(\"basic_model\".timestamp_field), '%d/%m/%Y %H:%M:%S%z') ELSE NULL END as TIMESTAMP)) as STRUCT(str_field VARCHAR, date_field DATE, timestamp_field TIMESTAMP))"),
229+
("another_model", AnotherModel, "TRY_CAST(STRUCT_PACK(\"unique_id\":= TRIM(\"another_model\".unique_id),\"basic_models\":= LIST_TRANSFORM(\"another_model\".basic_models, x -> STRUCT_PACK(\"str_field\":= TRIM(\"x\".str_field),\"date_field\":= TRY_CAST(CASE WHEN REGEXP_FULL_MATCH(TRIM(\"x\".date_field), '^[0-9]{4}\-[0-9]{2}\-[0-9]{2}$') THEN TRY_STRPTIME(TRIM(\"x\".date_field), '%Y-%m-%d') ELSE NULL END as DATE),\"timestamp_field\":= TRY_CAST(CASE WHEN REGEXP_FULL_MATCH(TRIM(\"x\".timestamp_field), '^[0-9]{2}/[0-9]{2}/[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]+(:[0-9]{2})?(\.[0-9]*)?$') THEN TRY_STRPTIME(TRIM(\"x\".timestamp_field), '%d/%m/%Y %H:%M:%S%z') ELSE NULL END as TIMESTAMP)))) as STRUCT(unique_id BIGINT, basic_models STRUCT(str_field VARCHAR, date_field DATE, timestamp_field TIMESTAMP)[]))")])
230230
def test_get_duckdb_cast_statement_from_annotation(field_name, field_type, cast_statement):
231231
assert get_duckdb_cast_statement_from_annotation(field_name, field_type) == cast_statement
232232

@@ -283,6 +283,25 @@ def test_relation_is_empty(temp_ddb_conn: DuckDBPyConnection):
283283
@pytest.mark.parametrize("date_format, expected_regex", [("%Y-%m-%d", r"^[0-9]{4}\-[0-9]{2}\-[0-9]{2}$"),
284284
("%Y-%m-%dT%H:%M:%S", r"^[0-9]{4}\-[0-9]{2}\-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}$"),
285285
("%m/%d/%Y", r"^[0-9]{2}/[0-9]{2}/[0-9]{4}$"),
286-
("%Y/%m/%d %H:%M:%S%z", r"^[0-9]{4}/[0-9]{2}/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}(\+|\-)?[0-9]+(\.[0-9]*)?$")])
286+
("%Y/%m/%d %H:%M:%S%z", r"^[0-9]{4}/[0-9]{2}/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]+(:[0-9]{2})?(\.[0-9]*)?$")])
287+
287288
def test_datetime_format_to_regex(date_format, expected_regex):
288-
assert datetime_format_to_regex(date_format) == expected_regex
289+
assert datetime_format_to_regex(date_format) == expected_regex
290+
291+
292+
@pytest.mark.parametrize("datetime_str, datetime_format, expected_value", [("2020-06-11", "%Y-%m-%d", datetime.date(2020,6,11)),
293+
("15/11/2018", "%d/%m/%Y", datetime.date(2018,11,15)),
294+
("27/14/2027", "%d/%m/%Y", None),
295+
("2014-05-27", "%d/%m/%y", None),
296+
("2020-03-15T13:24:54+0000", "%Y-%m-%dT%H:%M:%S%z", datetime.datetime(2020, 3, 15, 13, 24, 54)),
297+
("2020-03-15T13:24:54+00:00", "%Y-%m-%dT%H:%M:%S%z", datetime.datetime(2020, 3, 15, 13, 24, 54))])
298+
def test_datetime_casting(temp_ddb_conn: DuckDBPyConnection, datetime_str, datetime_format, expected_value):
299+
_, con = temp_ddb_conn
300+
if isinstance(expected_value, datetime.datetime):
301+
type_ = formatteddatetime(date_format=datetime_format)
302+
else:
303+
type_ = conformatteddate(date_format=datetime_format)
304+
cast_statement = get_duckdb_cast_statement_from_annotation("test", type_)
305+
rel = con.sql(f"SELECT '{datetime_str}' as test")
306+
check = rel.project(cast_statement).fetchone()[0]
307+
assert check == expected_value

‎tests/test_core_engine/test_backends/test_implementations/test_spark/test_spark_helpers.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
df_is_empty
3434
)
3535
from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
36-
from dve.metadata_parser.domain_types import conformatteddate
36+
from dve.metadata_parser.domain_types import conformatteddate, formatteddatetime
3737

3838
from .....fixtures import spark # pylint: disable=unused-import
3939

@@ -380,6 +380,22 @@ def test_df_is_empty(spark):
380380

381381
df5 = df4.union(spark.createDataFrame([(4, None, 8)], schema=_schema_w_ri))
382382
assert not df_is_empty(df5)
383+
384+
@pytest.mark.parametrize("datetime_str, datetime_format, expected_value", [("2020-06-11", "%Y-%m-%d", dt.date(2020,6,11)),
385+
("15/11/2018", "%d/%m/%Y", dt.date(2018,11,15)),
386+
("27/14/2027", "%d/%m/%Y", None),
387+
("2014-05-27", "%d/%m/%y", None),
388+
("2020-03-15T13:24:54+00:00", "%Y-%m-%dT%H:%M:%S%z", dt.datetime(2020, 3, 15, 13, 24, 54))])
389+
def test_datetime_casting(spark, datetime_str, datetime_format, expected_value):
390+
391+
if isinstance(expected_value, dt.datetime):
392+
type_ = formatteddatetime(date_format=datetime_format)
393+
else:
394+
type_ = conformatteddate(date_format=datetime_format)
395+
cast_statement = get_spark_cast_statement_from_annotation("test", type_)
396+
df = spark.createDataFrame([(datetime_str,)], schema=StructType([StructField("test", StringType())]))
397+
check = df.select(cast_statement.alias("test")).first().test
398+
assert check == expected_value
383399

384400

385401

0 commit comments

Comments
 (0)