diff --git a/src/pact/v3/util.py b/src/pact/v3/util.py new file mode 100644 index 0000000000..64a1783135 --- /dev/null +++ b/src/pact/v3/util.py @@ -0,0 +1,142 @@ +""" +Utility functions for Pact. + +This module defines a number of utility functions that are used in specific +contexts within the Pact library. These functions are not intended to be +used directly by consumers of the library, but are still made available for +reference. +""" + +import warnings + +_PYTHON_FORMAT_TO_JAVA_DATETIME = { + "a": "EEE", + "A": "EEEE", + "b": "MMM", + "B": "MMMM", + # c is locale dependent, so we can't convert it directly. + "d": "dd", + "f": "SSSSSS", + "G": "YYYY", + "H": "HH", + "I": "hh", + "j": "DDD", + "m": "MM", + "M": "mm", + "p": "a", + "S": "ss", + "u": "u", + "U": "ww", + "V": "ww", + # w is 0-indexed in Python, but 1-indexed in Java. + "W": "ww", + # x is locale dependent, so we can't convert it directly. + # X is locale dependent, so we can't convert it directly. + "y": "yy", + "Y": "yyyy", + "z": "Z", + "Z": "z", + "%": "%", + ":z": "XXX", +} + + +def strftime_to_simple_date_format(python_format: str) -> str: + """ + Convert a Python datetime format string to Java SimpleDateFormat format. + + Python uses [`strftime` + codes](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) + which are ultimately based on the C `strftime` function. Java uses + [`SimpleDateFormat` + codes](https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html) + which generally have corresponding codes, but with some differences. + + Note that this function strictly supports codes explicitly defined in the + Python documentation. Locale-dependent codes are not supported, and codes + supported by the underlying C library but not Python are not supported. For + examples, `%c`, `%x`, and `%X` are not supported as they are locale + dependent, and `%D` is not supported as it is not part of the Python + documentation (even though it may be supported by the underlying C and + therefore work in some Python implementations). + + Args: + python_format: + The Python datetime format string to convert. + + Returns: + The equivalent Java SimpleDateFormat format string. + """ + # Each Python format code is exactly two characters long, so we can + # safely iterate through the string. + idx = 0 + result: str = "" + escaped = False + + while idx < len(python_format): + c = python_format[idx] + idx += 1 + + if c == "%": + c = python_format[idx] + if escaped: + result += "'" + escaped = False + result += _format_code_to_java_format(c) + # Increment another time to skip the second character of the + # Python format code. + idx += 1 + continue + + if c == "'": + # In Java, single quotes are used to escape characters. + # To insert a single quote, we need to insert two single quotes. + # It doesn't matter if we're in an escape sequence or not, as + # Java treats them the same. + result += "''" + continue + + if not escaped and c.isalpha(): + result += "'" + escaped = True + result += c + + if escaped: + result += "'" + return result + + +def _format_code_to_java_format(code: str) -> str: + """ + Convert a single Python format code to a Java SimpleDateFormat format. + + Args: + code: + The Python format code to convert, without the leading `%`. This + will typically be a single character, but may be two characters + for some codes. + + Returns: + The equivalent Java SimpleDateFormat format string. + """ + if code in ["U", "V", "W"]: + warnings.warn( + f"The Java equivalent for `%{code}` is locale dependent.", + stacklevel=3, + ) + + # The following are locale-dependent, and aren't directly convertible. + if code in ["c", "x", "X"]: + msg = f"Cannot convert locale-dependent Python format code `%{code}` to Java" + raise ValueError(msg) + + # The following codes simply do not have a direct equivalent in Java. + if code in ["w"]: + msg = f"Python format code `%{code}` is not supported in Java" + raise ValueError(msg) + + if code in _PYTHON_FORMAT_TO_JAVA_DATETIME: + return _PYTHON_FORMAT_TO_JAVA_DATETIME[code] + + msg = f"Unsupported Python format code `%{code}`" + raise ValueError(msg) diff --git a/tests/v3/test_util.py b/tests/v3/test_util.py new file mode 100644 index 0000000000..1ef0393976 --- /dev/null +++ b/tests/v3/test_util.py @@ -0,0 +1,36 @@ +""" +Tests of pact.v3.util functions. +""" + +import pytest + +from pact.v3.util import strftime_to_simple_date_format + + +def test_convert_python_to_java_datetime_format_basic() -> None: + assert strftime_to_simple_date_format("%Y-%m-%d") == "yyyy-MM-dd" + assert strftime_to_simple_date_format("%H:%M:%S") == "HH:mm:ss" + + +def test_convert_python_to_java_datetime_format_with_unsupported_code() -> None: + with pytest.raises( + ValueError, + match="Cannot convert locale-dependent Python format code `%c` to Java", + ): + strftime_to_simple_date_format("%c") + + +def test_convert_python_to_java_datetime_format_with_warning() -> None: + with pytest.warns( + UserWarning, match="The Java equivalent for `%U` is locale dependent." + ): + assert strftime_to_simple_date_format("%U") == "ww" + + +def test_convert_python_to_java_datetime_format_with_escape_characters() -> None: + assert strftime_to_simple_date_format("'%Y-%m-%d'") == "''yyyy-MM-dd''" + assert strftime_to_simple_date_format("%%Y") == "%'Y'" + + +def test_convert_python_to_java_datetime_format_with_single_quote() -> None: + assert strftime_to_simple_date_format("%Y'%m'%d") == "yyyy''MM''dd"