Skip to content

# dates

## `xsdata.utils.dates`

### `DateTimeParser`

XML Datetime parser.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`value` | `str` | The datetime string | required
`fmt` | `str` | The target format string | required

Attributes:

Name | Type | Description
--- | --- | ---
`vlen` |  | The length of the datetime string
`flen` |  | The length of the format string
`vidx` |  | The current position of the datetime string
`fidx` |  | The current position of the format string

Source code in `xsdata/utils/dates.py`

    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    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
    221
    222
    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

    class DateTimeParser:
        """XML Datetime parser.

        Args:
            value: The datetime string
            fmt: The target format string

        Attributes:
            vlen: The length of the datetime string
            flen: The length of the format string
            vidx: The current position of the datetime string
            fidx: The current position of the format string
        """

        def __init__(self, value: str, fmt: str):
            """Initialize a DateTimeParser instance."""
            self.format = fmt
            self.value = value
            self.vlen = len(value)
            self.flen = len(fmt)
            self.vidx = 0
            self.fidx = 0

        def parse(self) -> Iterator[int | None]:
            """Yield the parsed datetime string arguments."""
            try:
                while self.fidx < self.flen:
                    char = self.next_format_char()

                    if char != "%":
                        self.skip(char)
                    else:
                        var = self.next_format_char()
                        yield from self.parse_var(var)

                if self.vidx != self.vlen:
                    raise ValueError

            except Exception:
                raise ValueError(
                    f"String '{self.value}' does not match format '{self.format}'"
                )

        def next_format_char(self) -> str:
            """Return the next format character to evaluate."""
            char = self.format[self.fidx]
            self.fidx += 1
            return char

        def has_more(self) -> bool:
            """Return whether the value is not fully parsed yet."""
            return self.vidx < self.vlen

        def peek(self) -> str:
            """Return the current evaluated character of the datetime string."""
            return self.value[self.vidx]

        def skip(self, char: str) -> None:
            """Validate and skip over the given char."""
            if not self.has_more() or self.peek() != char:
                raise ValueError

            self.vidx += 1

        def parse_var(self, var: str) -> Iterator[int | None]:
            """Parse the given var from the datetime string."""
            if var in SIMPLE_TWO_DIGITS_FORMATS:
                yield self.parse_digits(2)
            elif var == "Y":
                yield self.parse_year()
            elif var == "S":
                yield self.parse_digits(2)

                yield self.parse_fractional_second()
            elif var == "z":
                yield self.parse_offset()
            else:
                raise ValueError

        def parse_year(self) -> int:
            """Parse the year argument."""
            negative = False
            if self.peek() == "-":
                self.vidx += 1
                negative = True

            start = self.vidx
            year = self.parse_minimum_digits(4)
            end = self.vidx
            raw = self.value[start:end]

            leading_zeros = len(raw) - len(raw.lstrip("0"))
            if (
                (leading_zeros == 1 and year > 999)
                or (leading_zeros == 2 and year > 99)
                or (leading_zeros == 3 and year > 9)
                or (leading_zeros == 4 and year > 0)
                or (leading_zeros > 4)
            ):
                raise ValueError

            if negative:
                return -year

            return year

        def parse_fractional_second(self) -> int:
            """Parse the fractional second argument."""
            if self.has_more() and self.peek() == ".":
                self.vidx += 1
                return self.parse_fixed_digits(9)

            return 0

        def parse_digits(self, digits: int) -> int:
            """Parse the given number of digits."""
            start = self.vidx
            self.vidx += digits
            return int(self.value[start : self.vidx])

        def parse_minimum_digits(self, min_digits: int) -> int:
            """Parse until the next character is not a digit."""
            start = self.vidx
            self.vidx += min_digits

            while self.has_more() and self.peek().isdigit():
                self.vidx += 1

            return int(self.value[start : self.vidx])

        def parse_fixed_digits(self, max_digits: int) -> int:
            """Parse a fixed number of digits."""
            start = self.vidx
            just = max_digits
            while max_digits and self.has_more() and self.peek().isdigit():
                self.vidx += 1
                max_digits -= 1

            return int(self.value[start : self.vidx].ljust(just, "0"))

        def parse_offset(self) -> int | None:
            """Parse the xml timezone offset as minutes."""
            if not self.has_more():
                return None

            ctrl = self.peek()
            if ctrl == "Z":
                self.vidx += 1
                return 0

            if ctrl in ("-", "+"):
                self.vidx += 1
                offset = self.parse_digits(2) * 60
                self.skip(":")
                offset += self.parse_digits(2)
                offset *= -1 if ctrl == "-" else 1
                return offset

            raise ValueError

#### `__init__(value, fmt)`

Initialize a DateTimeParser instance.

Source code in `xsdata/utils/dates.py`

    136
    137
    138
    139
    140
    141
    142
    143

    def __init__(self, value: str, fmt: str):
        """Initialize a DateTimeParser instance."""
        self.format = fmt
        self.value = value
        self.vlen = len(value)
        self.flen = len(fmt)
        self.vidx = 0
        self.fidx = 0

#### `parse()`

Yield the parsed datetime string arguments.

Source code in `xsdata/utils/dates.py`

    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163

    def parse(self) -> Iterator[int | None]:
        """Yield the parsed datetime string arguments."""
        try:
            while self.fidx < self.flen:
                char = self.next_format_char()

                if char != "%":
                    self.skip(char)
                else:
                    var = self.next_format_char()
                    yield from self.parse_var(var)

            if self.vidx != self.vlen:
                raise ValueError

        except Exception:
            raise ValueError(
                f"String '{self.value}' does not match format '{self.format}'"
            )

#### `next_format_char()`

Return the next format character to evaluate.

Source code in `xsdata/utils/dates.py`

    165
    166
    167
    168
    169

    def next_format_char(self) -> str:
        """Return the next format character to evaluate."""
        char = self.format[self.fidx]
        self.fidx += 1
        return char

#### `has_more()`

Return whether the value is not fully parsed yet.

Source code in `xsdata/utils/dates.py`

    171
    172
    173

    def has_more(self) -> bool:
        """Return whether the value is not fully parsed yet."""
        return self.vidx < self.vlen

#### `peek()`

Return the current evaluated character of the datetime string.

Source code in `xsdata/utils/dates.py`

    175
    176
    177

    def peek(self) -> str:
        """Return the current evaluated character of the datetime string."""
        return self.value[self.vidx]

#### `skip(char)`

Validate and skip over the given char.

Source code in `xsdata/utils/dates.py`

    179
    180
    181
    182
    183
    184

    def skip(self, char: str) -> None:
        """Validate and skip over the given char."""
        if not self.has_more() or self.peek() != char:
            raise ValueError

        self.vidx += 1

#### `parse_var(var)`

Parse the given var from the datetime string.

Source code in `xsdata/utils/dates.py`

    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199

    def parse_var(self, var: str) -> Iterator[int | None]:
        """Parse the given var from the datetime string."""
        if var in SIMPLE_TWO_DIGITS_FORMATS:
            yield self.parse_digits(2)
        elif var == "Y":
            yield self.parse_year()
        elif var == "S":
            yield self.parse_digits(2)

            yield self.parse_fractional_second()
        elif var == "z":
            yield self.parse_offset()
        else:
            raise ValueError

#### `parse_year()`

Parse the year argument.

Source code in `xsdata/utils/dates.py`

    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226

    def parse_year(self) -> int:
        """Parse the year argument."""
        negative = False
        if self.peek() == "-":
            self.vidx += 1
            negative = True

        start = self.vidx
        year = self.parse_minimum_digits(4)
        end = self.vidx
        raw = self.value[start:end]

        leading_zeros = len(raw) - len(raw.lstrip("0"))
        if (
            (leading_zeros == 1 and year > 999)
            or (leading_zeros == 2 and year > 99)
            or (leading_zeros == 3 and year > 9)
            or (leading_zeros == 4 and year > 0)
            or (leading_zeros > 4)
        ):
            raise ValueError

        if negative:
            return -year

        return year

#### `parse_fractional_second()`

Parse the fractional second argument.

Source code in `xsdata/utils/dates.py`

    228
    229
    230
    231
    232
    233
    234

    def parse_fractional_second(self) -> int:
        """Parse the fractional second argument."""
        if self.has_more() and self.peek() == ".":
            self.vidx += 1
            return self.parse_fixed_digits(9)

        return 0

#### `parse_digits(digits)`

Parse the given number of digits.

Source code in `xsdata/utils/dates.py`

    236
    237
    238
    239
    240

    def parse_digits(self, digits: int) -> int:
        """Parse the given number of digits."""
        start = self.vidx
        self.vidx += digits
        return int(self.value[start : self.vidx])

#### `parse_minimum_digits(min_digits)`

Parse until the next character is not a digit.

Source code in `xsdata/utils/dates.py`

    242
    243
    244
    245
    246
    247
    248
    249
    250

    def parse_minimum_digits(self, min_digits: int) -> int:
        """Parse until the next character is not a digit."""
        start = self.vidx
        self.vidx += min_digits

        while self.has_more() and self.peek().isdigit():
            self.vidx += 1

        return int(self.value[start : self.vidx])

#### `parse_fixed_digits(max_digits)`

Parse a fixed number of digits.

Source code in `xsdata/utils/dates.py`

    252
    253
    254
    255
    256
    257
    258
    259
    260

    def parse_fixed_digits(self, max_digits: int) -> int:
        """Parse a fixed number of digits."""
        start = self.vidx
        just = max_digits
        while max_digits and self.has_more() and self.peek().isdigit():
            self.vidx += 1
            max_digits -= 1

        return int(self.value[start : self.vidx].ljust(just, "0"))

#### `parse_offset()`

Parse the xml timezone offset as minutes.

Source code in `xsdata/utils/dates.py`

    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280

    def parse_offset(self) -> int | None:
        """Parse the xml timezone offset as minutes."""
        if not self.has_more():
            return None

        ctrl = self.peek()
        if ctrl == "Z":
            self.vidx += 1
            return 0

        if ctrl in ("-", "+"):
            self.vidx += 1
            offset = self.parse_digits(2) * 60
            self.skip(":")
            offset += self.parse_digits(2)
            offset *= -1 if ctrl == "-" else 1
            return offset

        raise ValueError

### `parse_date_args(value, fmt)`

Parse the fmt args from the value.

Source code in `xsdata/utils/dates.py`

     7
     8
     9
    10
    11
    12
    13

    def parse_date_args(value: Any, fmt: str) -> Iterator[int | None]:
        """Parse the fmt args from the value."""
        if not isinstance(value, str):
            raise ValueError("")

        parser = DateTimeParser(value.strip(), fmt)
        return parser.parse()

### `calculate_timezone(offset)`

Return a timezone instance by the given hours offset.

Source code in `xsdata/utils/dates.py`

    16
    17
    18
    19
    20
    21
    22
    23
    24

    def calculate_timezone(offset: int | None) -> datetime.timezone | None:
        """Return a timezone instance by the given hours offset."""
        if offset is None:
            return None

        if offset == 0:
            return datetime.timezone.utc

        return datetime.timezone(datetime.timedelta(minutes=offset))

### `calculate_offset(obj)`

Convert the datetime offset to signed minutes.

Source code in `xsdata/utils/dates.py`

    27
    28
    29
    30
    31
    32
    33

    def calculate_offset(obj: datetime.time | datetime.datetime) -> int | None:
        """Convert the datetime offset to signed minutes."""
        offset = obj.utcoffset()
        if offset is None:
            return None

        return int(offset.total_seconds() // 60)

### `format_date(year, month, day)`

Return a xml formatted signed date.

Source code in `xsdata/utils/dates.py`

    36
    37
    38
    39
    40
    41
    42
    43
    44

    def format_date(year: int, month: int, day: int) -> str:
        """Return a xml formatted signed date."""
        if year < 0:
            year = -year
            sign = "-"
        else:
            sign = ""

        return f"{sign}{year:04d}-{month:02d}-{day:02d}"

### `format_time(hour, minute, second, fractional_second)`

Return a xml formatted time.

Source code in `xsdata/utils/dates.py`

    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60

    def format_time(hour: int, minute: int, second: int, fractional_second: int) -> str:
        """Return a xml formatted time."""
        if not fractional_second:
            return f"{hour:02d}:{minute:02d}:{second:02d}"

        microsecond, nano = divmod(fractional_second, 1000)
        if nano:
            return f"{hour:02d}:{minute:02d}:{second:02d}.{fractional_second:09d}"

        milli, micro = divmod(microsecond, 1000)
        if micro:
            return f"{hour:02d}:{minute:02d}:{second:02d}.{microsecond:06d}"

        return f"{hour:02d}:{minute:02d}:{second:02d}.{milli:03d}"

### `format_offset(offset)`

Return a xml formatted time offset.

Source code in `xsdata/utils/dates.py`

    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79

    def format_offset(offset: int | None) -> str:
        """Return a xml formatted time offset."""
        if offset is None:
            return ""

        if offset == 0:
            return "Z"

        if offset < 0:
            sign = "-"
            offset = -offset
        else:
            sign = "+"

        hh, mm = divmod(offset, 60)

        return f"{sign}{hh:02d}:{mm:02d}"

### `monthlen(year, month)`

Return the number of days for a specific month and year.

Source code in `xsdata/utils/dates.py`

    86
    87
    88

    def monthlen(year: int, month: int) -> int:
        """Return the number of days for a specific month and year."""
        return mdays[month] + (month == 2 and isleap(year))

### `validate_date(year, month, day)`

Validate the given year, month day is a valid date.

Source code in `xsdata/utils/dates.py`

    91
    92
    93
    94
    95
    96
    97
    98

    def validate_date(year: int, month: int, day: int) -> None:
        """Validate the given year, month day is a valid date."""
        if not 1 <= month <= 12:
            raise ValueError("Month must be in 1..12")

        max_days = monthlen(year, month)
        if not 1 <= day <= max_days:
            raise ValueError(f"Day must be in 1..{max_days}")

### `validate_time(hour, minute, second, franctional_second)`

Validate the time args are valid.

Source code in `xsdata/utils/dates.py`

    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116

    def validate_time(hour: int, minute: int, second: int, franctional_second: int) -> None:
        """Validate the time args are valid."""
        if not 0 <= hour <= 24:
            raise ValueError("Hour must be in 0..24")

        if hour == 24 and (minute != 0 or second != 0 or franctional_second != 0):
            raise ValueError("Day time exceeded")

        if not 0 <= minute <= 59:
            raise ValueError("Minute must be in 0..59")

        if not 0 <= second <= 59:
            raise ValueError("Second must be in 0..59")

        if not 0 <= franctional_second <= 999999999:
            raise ValueError("Fractional second must be in 0..999999999")