Skip to content

Datatype

Data types (Boolean, Date, DateTime, Duration).

Classes:

Name Description
Boolean

Handles conversion between ODF boolean string representation

Date

Handles conversion between ODF date string representation and Python’s

DateTime

Handles conversion between ODF date-time string representation and

Duration

Handles conversion between ODF duration string representation

Functions:

Name Description
date_decode_heuristic

Heuristic to convert a string representation (e.g., from JSON) of a

decode_heuristic

Heuristic to convert a string representation (e.g., from JSON) of a

Attributes:

Name Type Description
DATETIME_FORMAT
DATETIME_FORMAT_MICRO
DATE_FORMAT
DURATION_FORMAT

DATETIME_FORMAT module-attribute

DATETIME_FORMAT = DATE_FORMAT + 'T%H:%M:%S'

DATETIME_FORMAT_MICRO module-attribute

DATETIME_FORMAT_MICRO = DATETIME_FORMAT + '.%f'

DATE_FORMAT module-attribute

DATE_FORMAT = '%Y-%m-%d'

DURATION_FORMAT module-attribute

DURATION_FORMAT = 'PT%02dH%02dM%02dS'

Boolean

Handles conversion between ODF boolean string representation (‘true’, ‘false’) and Python’s native bool type.

Methods:

Name Description
decode

Decode an ODF boolean string to a Python boolean.

encode

Encode a Python boolean (or boolean-like string/bytes) to an ODF

Source code in odfdo/datatype.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class Boolean:
    """Handles conversion between ODF boolean string representation
    ('true', 'false') and Python's native `bool` type.
    """

    @staticmethod
    def decode(data: str | bool | None) -> bool:
        """Decode an ODF boolean string to a Python boolean.

        Args:
            data: The string to decode, expected to be 'true' or 'false',
                or a bool or None.

        Returns:
            bool: `True` if data is 'true', `False` if data is 'false'.

        Raises:
            ValueError: If the input string is not a valid ODF boolean
                ('true' or 'false'), and is neither a bool nor None.
        """
        match data:
            case bool():
                return data
            case None:
                return False
            case "true":
                return True
            case "false":
                return False
            case _:
                raise ValueError(f"boolean {data!r} is invalid")

    @staticmethod
    def encode(value: bool | str | bytes | int | float | Decimal | None) -> str:
        """Encode a Python boolean (or boolean-like string/bytes) to an ODF
        boolean string.

        Args:
            value: The value to encode. Can be a Python `bool`, a string
                ('true', 'false' case-insensitive), or bytes.

        Returns:
            str: The ODF boolean string ('true' or 'false').

        Raises:
            TypeError: If the input value cannot be interpreted as a boolean.
        """
        if isinstance(value, bytes):
            value = value.decode()
        elif isinstance(value, int | float | Decimal):
            value = bool(value)
        if value is True or str(value).lower() == "true":
            return "true"
        elif value is False or str(value).lower() == "false":
            return "false"
        raise TypeError(f"{value!r} is not a boolean")

decode staticmethod

decode(data: str | bool | None) -> bool

Decode an ODF boolean string to a Python boolean.

Parameters:

Name Type Description Default
data str | bool | None

The string to decode, expected to be ‘true’ or ‘false’, or a bool or None.

required

Returns:

Name Type Description
bool bool

True if data is ‘true’, False if data is ‘false’.

Raises:

Type Description
ValueError

If the input string is not a valid ODF boolean (‘true’ or ‘false’), and is neither a bool nor None.

Source code in odfdo/datatype.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@staticmethod
def decode(data: str | bool | None) -> bool:
    """Decode an ODF boolean string to a Python boolean.

    Args:
        data: The string to decode, expected to be 'true' or 'false',
            or a bool or None.

    Returns:
        bool: `True` if data is 'true', `False` if data is 'false'.

    Raises:
        ValueError: If the input string is not a valid ODF boolean
            ('true' or 'false'), and is neither a bool nor None.
    """
    match data:
        case bool():
            return data
        case None:
            return False
        case "true":
            return True
        case "false":
            return False
        case _:
            raise ValueError(f"boolean {data!r} is invalid")

encode staticmethod

encode(
    value: bool
    | str
    | bytes
    | int
    | float
    | Decimal
    | None,
) -> str

Encode a Python boolean (or boolean-like string/bytes) to an ODF boolean string.

Parameters:

Name Type Description Default
value bool | str | bytes | int | float | Decimal | None

The value to encode. Can be a Python bool, a string (‘true’, ‘false’ case-insensitive), or bytes.

required

Returns:

Name Type Description
str str

The ODF boolean string (‘true’ or ‘false’).

Raises:

Type Description
TypeError

If the input value cannot be interpreted as a boolean.

Source code in odfdo/datatype.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@staticmethod
def encode(value: bool | str | bytes | int | float | Decimal | None) -> str:
    """Encode a Python boolean (or boolean-like string/bytes) to an ODF
    boolean string.

    Args:
        value: The value to encode. Can be a Python `bool`, a string
            ('true', 'false' case-insensitive), or bytes.

    Returns:
        str: The ODF boolean string ('true' or 'false').

    Raises:
        TypeError: If the input value cannot be interpreted as a boolean.
    """
    if isinstance(value, bytes):
        value = value.decode()
    elif isinstance(value, int | float | Decimal):
        value = bool(value)
    if value is True or str(value).lower() == "true":
        return "true"
    elif value is False or str(value).lower() == "false":
        return "false"
    raise TypeError(f"{value!r} is not a boolean")

Date

Handles conversion between ODF date string representation and Python’s datetime.date type.

Assumes ISO 8601 format (YYYY-MM-DD) for ODF dates.

Methods:

Name Description
decode

Decode an ODF date string or date/datetime object to a Python

encode

Encode a Python datetime or date object to an ODF date string.

Source code in odfdo/datatype.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
class Date:
    """Handles conversion between ODF date string representation and Python's
    `datetime.date` type.

    Assumes ISO 8601 format (YYYY-MM-DD) for ODF dates.
    """

    @staticmethod
    def decode(data: str | date | datetime) -> date:
        """Decode an ODF date string or date/datetime object to a Python
        `date` object.

        Idempotent on `date` object.
        If a `datetime` object is provided, it is converted to `date`.

        Args:
            data: The date string (YYYY-MM-DD or ISO 8601) or date/datetime
                object to decode.

        Returns:
            date: A `datetime.date` object representing the decoded date.
        """
        if isinstance(data, datetime):
            return data.date()
        if isinstance(data, date):
            return data
        if not isinstance(data, str):
            raise TypeError(f"date {data!r} is invalid")
        data_string = data.strip()
        if "T" in data_string or " " in data_string:
            with contextlib.suppress(ValueError):
                return DateTime.decode(data_string.replace(" ", "T")).date()
        return date.fromisoformat(data_string)

    @staticmethod
    def encode(value: datetime | date) -> str:
        """Encode a Python `datetime` or `date` object to an ODF date string.

        The output string is formatted as "YYYY-MM-DD". If a `datetime` is
        provided, only its date component is encoded.

        Args:
            value: The `datetime` or `date` object to encode.

        Returns:
            str: The ODF date string (e.g., "2024-01-31").
        """
        if isinstance(value, datetime):
            return value.date().isoformat()
        if isinstance(value, date):
            return value.isoformat()
        raise TypeError(f"Cannot encode {value!r} as Date")

decode staticmethod

decode(data: str | date | datetime) -> date

Decode an ODF date string or date/datetime object to a Python date object.

Idempotent on date object. If a datetime object is provided, it is converted to date.

Parameters:

Name Type Description Default
data str | date | datetime

The date string (YYYY-MM-DD or ISO 8601) or date/datetime object to decode.

required

Returns:

Name Type Description
date date

A datetime.date object representing the decoded date.

Source code in odfdo/datatype.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
@staticmethod
def decode(data: str | date | datetime) -> date:
    """Decode an ODF date string or date/datetime object to a Python
    `date` object.

    Idempotent on `date` object.
    If a `datetime` object is provided, it is converted to `date`.

    Args:
        data: The date string (YYYY-MM-DD or ISO 8601) or date/datetime
            object to decode.

    Returns:
        date: A `datetime.date` object representing the decoded date.
    """
    if isinstance(data, datetime):
        return data.date()
    if isinstance(data, date):
        return data
    if not isinstance(data, str):
        raise TypeError(f"date {data!r} is invalid")
    data_string = data.strip()
    if "T" in data_string or " " in data_string:
        with contextlib.suppress(ValueError):
            return DateTime.decode(data_string.replace(" ", "T")).date()
    return date.fromisoformat(data_string)

encode staticmethod

encode(value: datetime | date) -> str

Encode a Python datetime or date object to an ODF date string.

The output string is formatted as “YYYY-MM-DD”. If a datetime is provided, only its date component is encoded.

Parameters:

Name Type Description Default
value datetime | date

The datetime or date object to encode.

required

Returns:

Name Type Description
str str

The ODF date string (e.g., “2024-01-31”).

Source code in odfdo/datatype.py
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
@staticmethod
def encode(value: datetime | date) -> str:
    """Encode a Python `datetime` or `date` object to an ODF date string.

    The output string is formatted as "YYYY-MM-DD". If a `datetime` is
    provided, only its date component is encoded.

    Args:
        value: The `datetime` or `date` object to encode.

    Returns:
        str: The ODF date string (e.g., "2024-01-31").
    """
    if isinstance(value, datetime):
        return value.date().isoformat()
    if isinstance(value, date):
        return value.isoformat()
    raise TypeError(f"Cannot encode {value!r} as Date")

DateTime

Handles conversion between ODF date-time string representation and Python’s datetime.datetime type.

Assumes ISO 8601 format for ODF date-times.

Methods:

Name Description
decode

Decode an ODF date-time string or date/datetime object to a Python datetime.datetime object.

encode

Encode a Python datetime or date object to an ODF date-time

Source code in odfdo/datatype.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
class DateTime:
    """Handles conversion between ODF date-time string representation and
    Python's `datetime.datetime` type.

    Assumes ISO 8601 format for ODF date-times.
    """

    @staticmethod
    def decode(data: str | datetime | date) -> datetime:
        """Decode an ODF date-time string  or date/datetime object to a Python `datetime.datetime` object.

        Idempotent on `datetime` object.
        If a `datetime.date` object is provided, it is converted to a
        `datetime.datetime` object at 00:00:00.

        Args:
            data: The date-time string or date/datetime object to decode.

        Returns:
            datetime: A `datetime.datetime` object.
        """

        def _decode_39_310(data1: str) -> datetime:  # pragma: nocover
            if data1.endswith("Z"):
                data1 = data1[:-1] + "+00:00"
            try:
                return datetime.fromisoformat(data1)
            except ValueError as e:
                if "microsecond must be" in str(e) or "Invalid isoformat string" in str(
                    e
                ):
                    if len(data1) == 29:
                        return datetime.fromisoformat(data1[:26])
                    if len(data1) == 35:
                        return datetime.fromisoformat(data1[:26] + data1[-6:])
                raise

        if isinstance(data, datetime):
            return data
        if isinstance(data, date):
            return datetime.combine(data, datetime.min.time())
        if not isinstance(data, str):
            raise TypeError(f"datetime {data!r} is invalid")

        data_string = data.strip()

        try:
            return datetime.fromisoformat(data_string)
        except ValueError:
            if " " in data_string:
                with contextlib.suppress(ValueError):
                    return datetime.fromisoformat(data_string.replace(" ", "T"))
            # maybe python 3.9 pr 3.10
            if sys.version_info.minor in {9, 10}:  # pragma: nocover
                return _decode_39_310(data_string)
            raise

    @staticmethod
    def encode(value: datetime | date) -> str:
        """Encode a Python `datetime` or `date` object to an ODF date-time
        string.

        If a `datetime.date` object is provided, it is converted to a
        `datetime.datetime` object at 00:00:00.

        The output string is formatted in ISO 8601. UTC offsets
        (e.g., "+00:00") are converted to the canonical 'Z' representation.

        Args:
            value: The `datetime` or `date` object to encode.

        Returns:
            str: The ODF date-time string (e.g., "YYYY-MM-DDTHH:MM:SSZ").
        """
        if isinstance(value, datetime):
            dt = value
        elif isinstance(value, date):
            dt = datetime.combine(value, datetime.min.time())
        else:
            raise TypeError(f"Cannot encode {value!r} as DateTime")

        text = dt.isoformat()
        if text.endswith("+00:00"):
            # convert to canonical representation
            return text[:-6] + "Z"
        return text

decode staticmethod

decode(data: str | datetime | date) -> datetime

Decode an ODF date-time string or date/datetime object to a Python datetime.datetime object.

Idempotent on datetime object. If a datetime.date object is provided, it is converted to a datetime.datetime object at 00:00:00.

Parameters:

Name Type Description Default
data str | datetime | date

The date-time string or date/datetime object to decode.

required

Returns:

Name Type Description
datetime datetime

A datetime.datetime object.

Source code in odfdo/datatype.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
@staticmethod
def decode(data: str | datetime | date) -> datetime:
    """Decode an ODF date-time string  or date/datetime object to a Python `datetime.datetime` object.

    Idempotent on `datetime` object.
    If a `datetime.date` object is provided, it is converted to a
    `datetime.datetime` object at 00:00:00.

    Args:
        data: The date-time string or date/datetime object to decode.

    Returns:
        datetime: A `datetime.datetime` object.
    """

    def _decode_39_310(data1: str) -> datetime:  # pragma: nocover
        if data1.endswith("Z"):
            data1 = data1[:-1] + "+00:00"
        try:
            return datetime.fromisoformat(data1)
        except ValueError as e:
            if "microsecond must be" in str(e) or "Invalid isoformat string" in str(
                e
            ):
                if len(data1) == 29:
                    return datetime.fromisoformat(data1[:26])
                if len(data1) == 35:
                    return datetime.fromisoformat(data1[:26] + data1[-6:])
            raise

    if isinstance(data, datetime):
        return data
    if isinstance(data, date):
        return datetime.combine(data, datetime.min.time())
    if not isinstance(data, str):
        raise TypeError(f"datetime {data!r} is invalid")

    data_string = data.strip()

    try:
        return datetime.fromisoformat(data_string)
    except ValueError:
        if " " in data_string:
            with contextlib.suppress(ValueError):
                return datetime.fromisoformat(data_string.replace(" ", "T"))
        # maybe python 3.9 pr 3.10
        if sys.version_info.minor in {9, 10}:  # pragma: nocover
            return _decode_39_310(data_string)
        raise

encode staticmethod

encode(value: datetime | date) -> str

Encode a Python datetime or date object to an ODF date-time string.

If a datetime.date object is provided, it is converted to a datetime.datetime object at 00:00:00.

The output string is formatted in ISO 8601. UTC offsets (e.g., “+00:00”) are converted to the canonical ‘Z’ representation.

Parameters:

Name Type Description Default
value datetime | date

The datetime or date object to encode.

required

Returns:

Name Type Description
str str

The ODF date-time string (e.g., “YYYY-MM-DDTHH:MM:SSZ”).

Source code in odfdo/datatype.py
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
@staticmethod
def encode(value: datetime | date) -> str:
    """Encode a Python `datetime` or `date` object to an ODF date-time
    string.

    If a `datetime.date` object is provided, it is converted to a
    `datetime.datetime` object at 00:00:00.

    The output string is formatted in ISO 8601. UTC offsets
    (e.g., "+00:00") are converted to the canonical 'Z' representation.

    Args:
        value: The `datetime` or `date` object to encode.

    Returns:
        str: The ODF date-time string (e.g., "YYYY-MM-DDTHH:MM:SSZ").
    """
    if isinstance(value, datetime):
        dt = value
    elif isinstance(value, date):
        dt = datetime.combine(value, datetime.min.time())
    else:
        raise TypeError(f"Cannot encode {value!r} as DateTime")

    text = dt.isoformat()
    if text.endswith("+00:00"):
        # convert to canonical representation
        return text[:-6] + "Z"
    return text

Duration

Handles conversion between ODF duration string representation (ISO 8601 format) and Python’s datetime.timedelta type.

Methods:

Name Description
decode

Decode an ODF duration string (ISO 8601) to a Python

encode

Encode a Python datetime.timedelta object to an ODF duration

Source code in odfdo/datatype.py
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
class Duration:
    """Handles conversion between ODF duration string representation
    (ISO 8601 format) and Python's `datetime.timedelta` type.
    """

    @staticmethod
    def decode(data: str | timedelta) -> timedelta:
        """Decode an ODF duration string (ISO 8601) to a Python
        `datetime.timedelta` object.

        Idempotent on `timedelta` object.

        Args:
            data: The duration string to decode (e.g., "PT1H30M0S", "-P5D").

        Returns:
            timedelta: A `datetime.timedelta` object representing the decoded
                duration.

        Raises:
            ValueError: If the input string is not a valid ISO 8601 duration
                format.
        """
        if isinstance(data, timedelta):
            return data
        if not isinstance(data, str):
            raise TypeError(f"duration not valid {data!r}")
        if not (data.startswith("P") or data.startswith("-P")):
            raise ValueError(f"duration not valid {data!r}")
        if set(data) - set("-+P0123456789.YMWDHST,") or not any(
            c.isdigit() for c in data
        ):
            raise ValueError(f"duration not valid {data!r}")

        if data.startswith("P"):
            sign = 1
        else:
            sign = -1

        days = 0
        hours = 0
        minutes = 0
        seconds = 0

        buffer = ""
        for c in data:
            if c.isdigit():
                buffer += c
            elif c == "D":
                days = int(buffer)
                buffer = ""
            elif c == "H":
                hours = int(buffer)
                buffer = ""
            elif c == "M":
                minutes = int(buffer)
                buffer = ""
            elif c == "S":
                seconds = int(buffer)
                buffer = ""
                break
        if buffer != "":
            raise ValueError(f"duration not valid {data!r}")

        return timedelta(
            days=sign * days,
            hours=sign * hours,
            minutes=sign * minutes,
            seconds=sign * seconds,
        )

    @staticmethod
    def encode(value: timedelta) -> str:
        """Encode a Python `datetime.timedelta` object to an ODF duration
        string (ISO 8601).

        Args:
            value: The `datetime.timedelta` object to encode.

        Returns:
            str: The ODF duration string (e.g., "PT1H30M0S", "-P5D").

        Raises:
            TypeError: If the input value is not a `datetime.timedelta`
                object.
        """
        if not isinstance(value, timedelta):
            raise TypeError(f"duration must be a timedelta: {value!r}")

        days = value.days
        if days < 0:
            microseconds = -(
                (days * 24 * 60 * 60 + value.seconds) * 1_000_000 + value.microseconds
            )
            sign = "-"
        else:
            microseconds = (
                days * 24 * 60 * 60 + value.seconds
            ) * 1_000_000 + value.microseconds
            sign = ""

        hours = microseconds / (60 * 60 * 1_000_000)
        microseconds %= 60 * 60 * 1_000_000

        minutes = microseconds / (60 * 1_000_000)
        microseconds %= 60 * 1_000_000

        seconds = microseconds / 1_000_000

        return sign + DURATION_FORMAT % (hours, minutes, seconds)

decode staticmethod

decode(data: str | timedelta) -> timedelta

Decode an ODF duration string (ISO 8601) to a Python datetime.timedelta object.

Idempotent on timedelta object.

Parameters:

Name Type Description Default
data str | timedelta

The duration string to decode (e.g., “PT1H30M0S”, “-P5D”).

required

Returns:

Name Type Description
timedelta timedelta

A datetime.timedelta object representing the decoded duration.

Raises:

Type Description
ValueError

If the input string is not a valid ISO 8601 duration format.

Source code in odfdo/datatype.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
@staticmethod
def decode(data: str | timedelta) -> timedelta:
    """Decode an ODF duration string (ISO 8601) to a Python
    `datetime.timedelta` object.

    Idempotent on `timedelta` object.

    Args:
        data: The duration string to decode (e.g., "PT1H30M0S", "-P5D").

    Returns:
        timedelta: A `datetime.timedelta` object representing the decoded
            duration.

    Raises:
        ValueError: If the input string is not a valid ISO 8601 duration
            format.
    """
    if isinstance(data, timedelta):
        return data
    if not isinstance(data, str):
        raise TypeError(f"duration not valid {data!r}")
    if not (data.startswith("P") or data.startswith("-P")):
        raise ValueError(f"duration not valid {data!r}")
    if set(data) - set("-+P0123456789.YMWDHST,") or not any(
        c.isdigit() for c in data
    ):
        raise ValueError(f"duration not valid {data!r}")

    if data.startswith("P"):
        sign = 1
    else:
        sign = -1

    days = 0
    hours = 0
    minutes = 0
    seconds = 0

    buffer = ""
    for c in data:
        if c.isdigit():
            buffer += c
        elif c == "D":
            days = int(buffer)
            buffer = ""
        elif c == "H":
            hours = int(buffer)
            buffer = ""
        elif c == "M":
            minutes = int(buffer)
            buffer = ""
        elif c == "S":
            seconds = int(buffer)
            buffer = ""
            break
    if buffer != "":
        raise ValueError(f"duration not valid {data!r}")

    return timedelta(
        days=sign * days,
        hours=sign * hours,
        minutes=sign * minutes,
        seconds=sign * seconds,
    )

encode staticmethod

encode(value: timedelta) -> str

Encode a Python datetime.timedelta object to an ODF duration string (ISO 8601).

Parameters:

Name Type Description Default
value timedelta

The datetime.timedelta object to encode.

required

Returns:

Name Type Description
str str

The ODF duration string (e.g., “PT1H30M0S”, “-P5D”).

Raises:

Type Description
TypeError

If the input value is not a datetime.timedelta object.

Source code in odfdo/datatype.py
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
@staticmethod
def encode(value: timedelta) -> str:
    """Encode a Python `datetime.timedelta` object to an ODF duration
    string (ISO 8601).

    Args:
        value: The `datetime.timedelta` object to encode.

    Returns:
        str: The ODF duration string (e.g., "PT1H30M0S", "-P5D").

    Raises:
        TypeError: If the input value is not a `datetime.timedelta`
            object.
    """
    if not isinstance(value, timedelta):
        raise TypeError(f"duration must be a timedelta: {value!r}")

    days = value.days
    if days < 0:
        microseconds = -(
            (days * 24 * 60 * 60 + value.seconds) * 1_000_000 + value.microseconds
        )
        sign = "-"
    else:
        microseconds = (
            days * 24 * 60 * 60 + value.seconds
        ) * 1_000_000 + value.microseconds
        sign = ""

    hours = microseconds / (60 * 60 * 1_000_000)
    microseconds %= 60 * 60 * 1_000_000

    minutes = microseconds / (60 * 1_000_000)
    microseconds %= 60 * 1_000_000

    seconds = microseconds / 1_000_000

    return sign + DURATION_FORMAT % (hours, minutes, seconds)

date_decode_heuristic

date_decode_heuristic(
    data: str | bytes | datetime | date | timedelta | None,
) -> datetime | date

Heuristic to convert a string representation (e.g., from JSON) of a date, date-time, or duration into a Python date, datetime, or timedelta object.

Parameters:

Name Type Description Default
data str | bytes | datetime | date | timedelta | None

Value to decode.

required

Returns:

Type Description
datetime | date

datetime | date: Decoded or original data.

Raises:

Type Description
TypeError

If the result is not of type datetime | date.

Source code in odfdo/datatype.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def date_decode_heuristic(
    data: str | bytes | datetime | date | timedelta | None,
) -> datetime | date:
    """Heuristic to convert a string representation (e.g., from JSON) of a
    date, date-time, or duration into a Python `date`, `datetime`, or
    `timedelta` object.

    Args:
        data: Value to decode.

    Returns:
        datetime | date: Decoded or original `data`.

    Raises:
        TypeError: If the result is not of type datetime | date.
    """
    result = decode_heuristic(data)
    if not isinstance(result, date):
        msg = f"Cannot decode {data!r} as date or datetime"
        raise TypeError(msg)
    return result

decode_heuristic

decode_heuristic(
    data: str | bytes | datetime | date | timedelta | None,
) -> datetime | date | timedelta | str | bytes | None

Heuristic to convert a string representation (e.g., from JSON) of a date, date-time, or duration into a Python date, datetime, or timedelta object.

If data is already a date, datetime, or timedelta, it is returned unmodified. If data is a string (or bytes), ISO 8601 formats for duration, date-time, and date are attempted in sequence. If no conversion matches or data is not a string, data is returned unchanged.

Parameters:

Name Type Description Default
data str | bytes | datetime | date | timedelta | None

Value to decode.

required

Returns:

Type Description
datetime | date | timedelta | str | bytes | None

datetime | date | timedelta | str | None: Decoded or original data.

Source code in odfdo/datatype.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def decode_heuristic(
    data: str | bytes | datetime | date | timedelta | None,
) -> datetime | date | timedelta | str | bytes | None:
    """Heuristic to convert a string representation (e.g., from JSON) of a
    date, date-time, or duration into a Python `date`, `datetime`, or
    `timedelta` object.

    If `data` is already a `date`, `datetime`, or `timedelta`, it is returned
    unmodified. If `data` is a string (or bytes), ISO 8601 formats for
    duration, date-time, and date are attempted in sequence. If no conversion
    matches or `data` is not a string, `data` is returned unchanged.

    Args:
        data: Value to decode.

    Returns:
        datetime | date | timedelta | str | None: Decoded or original `data`.
    """
    if isinstance(data, datetime | date | timedelta):
        return data
    if isinstance(data, bytes):
        with contextlib.suppress(UnicodeDecodeError):
            data = data.decode()
    if not isinstance(data, str):
        return data

    data_string = data.strip()
    if not data_string:
        return data

    # ISO Duration (starts with P, -P, +P)
    if data_string.startswith(("P", "-P", "+P")):
        with contextlib.suppress(ValueError):
            return Duration.decode(data_string)

    # ISO DateTime (contains T or space between date and time)
    if "T" in data_string or " " in data_string:
        with contextlib.suppress(ValueError):
            return DateTime.decode(data_string.replace(" ", "T"))

    # ISO Date (YYYY-MM-DD)
    with contextlib.suppress(ValueError):
        return Date.decode(data_string)

    return data