""" Java text standard library APIs implemented in Python. java.text package - DateFormat, SimpleDateFormat, DecimalFormat, NumberFormat, etc. """ from .._core import Stub from .lang import RuntimeException import datetime as _datetime import time as _time import re as _re import math as _py_math import locale as _locale from decimal import Decimal as _Decimal IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) __all__ = [ "SimpleDateFormat", "DateFormat", "DateFormatSymbols", "DecimalFormat", "DecimalFormatSymbols", "NumberFormat", "ChoiceFormat", "MessageFormat", "FieldPosition", "ParsePosition", "ParseException", "Normalizer", "StringCharacterIterator", "CharacterIterator", "BreakIterator", "Collator", "CollationKey", "RuleBasedCollator", "Annotation", "AttributedCharacterIterator", "AttributedString", "Bidi", "Format", ] # ============================================================ # ParseException # ============================================================ class ParseException(Exception): def __init__(self, s, error_offset): super().__init__(s) self._error_offset = error_offset def getErrorOffset(self): return self._error_offset # ============================================================ # ParsePosition # ============================================================ class ParsePosition(Stub): def __init__(self, index): super().__init__() self._index = index self._error_index = -1 def getIndex(self): return self._index def setIndex(self, index): self._index = index def getErrorIndex(self): return self._error_index def setErrorIndex(self, ei): self._error_index = ei # ============================================================ # FieldPosition # ============================================================ class FieldPosition(Stub): def __init__(self, field): super().__init__() self._field = field self._begin = 0 self._end = 0 def getField(self): return self._field def getBeginIndex(self): return self._begin def getEndIndex(self): return self._end def setBeginIndex(self, bi): self._begin = bi def setEndIndex(self, ei): self._end = ei # ============================================================ # Format (base) # ============================================================ class Format(Stub): def format(self, obj, to_append_to=None, pos=None): raise NotImplementedError def parseObject(self, source, pos=None): raise NotImplementedError # ============================================================ # DateFormat # ============================================================ class DateFormat(Format): SHORT = 3 MEDIUM = 2 LONG = 1 FULL = 0 DEFAULT = 2 def __init__(self): super().__init__() self._calendar = None self._number_format = None self._lenient = False self._timezone = None @staticmethod def getDateInstance(style=2, locale=None): return SimpleDateFormat("yyyy-MM-dd") @staticmethod def getTimeInstance(style=2, locale=None): return SimpleDateFormat("HH:mm:ss") @staticmethod def getDateTimeInstance(date_style=2, time_style=2, locale=None): return SimpleDateFormat("yyyy-MM-dd HH:mm:ss") @staticmethod def getInstance(): return SimpleDateFormat("yyyy-MM-dd HH:mm:ss") def format(self, date, to_append_to=None, pos=None): raise NotImplementedError def parse(self, source, pos=None): raise NotImplementedError def setLenient(self, lenient): self._lenient = lenient def isLenient(self): return self._lenient def getCalendar(self): return self._calendar def setCalendar(self, cal): self._calendar = cal def getNumberFormat(self): return self._number_format def setNumberFormat(self, nf): self._number_format = nf def getTimeZone(self): return self._timezone def setTimeZone(self, tz): self._timezone = tz # ============================================================ # DateFormatSymbols # ============================================================ class DateFormatSymbols(Stub): def __init__(self, locale=None): super().__init__() self._months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ] self._short_months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ] self._weekdays = [ "", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", ] self._short_weekdays = [ "", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", ] self._ampm = ["AM", "PM"] self._eras = ["BC", "AD"] self._zone_strings = [] self._local_pattern_chars = "GyMdkHmsSEDFwWahKzZ" def getMonths(self): return self._months def setMonths(self, months): self._months = months def getShortMonths(self): return self._short_months def setShortMonths(self, months): self._short_months = months def getWeekdays(self): return self._weekdays def setWeekdays(self, wd): self._weekdays = wd def getShortWeekdays(self): return self._short_weekdays def setShortWeekdays(self, wd): self._short_weekdays = wd def getAmPmStrings(self): return self._ampm def setAmPmStrings(self, ap): self._ampm = ap def getZoneStrings(self): return self._zone_strings def setZoneStrings(self, zs): self._zone_strings = zs def getLocalPatternChars(self): return self._local_pattern_chars def setLocalPatternChars(self, chars): self._local_pattern_chars = chars def getEras(self): return self._eras def setEras(self, eras): self._eras = eras # ============================================================ # SimpleDateFormat # ============================================================ _JAVA_TO_PYTHON_FORMAT = { "yyyy": "%Y", "yy": "%y", "MM": "%m", "dd": "%d", "HH": "%H", "mm": "%M", "ss": "%S", "SSS": "%f", "a": "%p", "EEE": "%a", "EEEE": "%A", "D": "%j", "zzz": "%Z", "z": "%Z", "Z": "%z", } class SimpleDateFormat(DateFormat): def __init__(self, pattern, locale=None): super().__init__() self._pattern = pattern self._locale = locale self._dfs = DateFormatSymbols(locale) self._lenient = False def applyPattern(self, pattern): self._pattern = pattern def toPattern(self): return self._pattern def toLocalizedPattern(self): return self._pattern def applyLocalizedPattern(self, pattern): self._pattern = pattern def _java_to_py(self, pattern): py_pattern = pattern sorted_keys = sorted(_JAVA_TO_PYTHON_FORMAT.keys(), key=lambda x: -len(x)) for java_fmt in sorted_keys: py_fmt = _JAVA_TO_PYTHON_FORMAT[java_fmt] py_pattern = py_pattern.replace(java_fmt, py_fmt) py_pattern = py_pattern.replace("'", "") return py_pattern def _to_datetime(self, date): if isinstance(date, _datetime.datetime): return date if isinstance(date, _datetime.date): return _datetime.datetime.combine(date, _datetime.time()) try: ts = date.getTime() / 1000.0 return _datetime.datetime.fromtimestamp(ts) except AttributeError: return _datetime.datetime.now() def format(self, date, to_append_to=None, pos=None): py_pattern = self._java_to_py(self._pattern) dt = self._to_datetime(date) result = dt.strftime(py_pattern) if to_append_to is not None: sb = str(to_append_to) idx = len(sb) to_append_to.append(result) if pos is not None: pos.setBeginIndex(idx) pos.setEndIndex(idx + len(result)) return to_append_to return result def parse(self, source, pos=None): py_pattern = self._java_to_py(self._pattern) try: dt = _datetime.datetime.strptime(source, py_pattern) if pos is not None: pos.setIndex(len(source)) return dt except ValueError: if pos is not None: pos.setErrorIndex(0) raise ParseException(f"Unparseable date: {source}", 0) def setLenient(self, lenient): self._lenient = lenient def isLenient(self): return self._lenient def setTimeZone(self, tz): self._timezone = tz def getTimeZone(self): return self._timezone def setCalendar(self, cal): self._calendar = cal def getCalendar(self): return self._calendar def setNumberFormat(self, nf): self._number_format = nf def getNumberFormat(self): return self._number_format def getDateFormatSymbols(self): return self._dfs def setDateFormatSymbols(self, dfs): self._dfs = dfs # ============================================================ # DecimalFormatSymbols # ============================================================ class DecimalFormatSymbols(Stub): def __init__(self, locale=None): super().__init__() self._decimal_sep = "." self._grouping_sep = "," self._pattern_sep = ";" self._minus_sign = "-" self._infinity = "\u221e" self._nan = "NaN" self._percent = "%" self._per_mill = "\u2030" self._zero_digit = "0" self._digit = "#" self._exponential_sep = "E" self._monetary_decimal_sep = "." self._intl_currency_symbol = "USD" self._currency = "USD" def getDecimalSeparator(self): return self._decimal_sep def setDecimalSeparator(self, c): self._decimal_sep = c def getGroupingSeparator(self): return self._grouping_sep def setGroupingSeparator(self, c): self._grouping_sep = c def getPatternSeparator(self): return self._pattern_sep def setPatternSeparator(self, c): self._pattern_sep = c def getMinusSign(self): return self._minus_sign def setMinusSign(self, c): self._minus_sign = c def getInfinity(self): return self._infinity def setInfinity(self, s): self._infinity = s def getNaN(self): return self._nan def setNaN(self, s): self._nan = s def getPercent(self): return self._percent def setPercent(self, c): self._percent = c def getPerMill(self): return self._per_mill def setPerMill(self, c): self._per_mill = c def getZeroDigit(self): return self._zero_digit def setZeroDigit(self, c): self._zero_digit = c def getDigit(self): return self._digit def setDigit(self, c): self._digit = c def getExponentialSeparator(self): return self._exponential_sep def setExponentialSeparator(self, s): self._exponential_sep = s def getMonetaryDecimalSeparator(self): return self._monetary_decimal_sep def setMonetaryDecimalSeparator(self, c): self._monetary_decimal_sep = c def getInternationalCurrencySymbol(self): return self._intl_currency_symbol def setInternationalCurrencySymbol(self, s): self._intl_currency_symbol = s def getCurrency(self): return self._currency def setCurrency(self, c): self._currency = c # ============================================================ # DecimalFormat # ============================================================ class NumberFormat(Format): def __init__(self): super().__init__() self._grouping_used = True self._max_int_digits = 2000000000 self._min_int_digits = 1 self._max_frac_digits = 3 self._min_frac_digits = 0 self._parse_integer_only = False self._rounding_mode = 4 @staticmethod def getInstance(locale=None): return DecimalFormat("#,##0.###") @staticmethod def getNumberInstance(locale=None): return DecimalFormat("#,##0.###") @staticmethod def getCurrencyInstance(locale=None): return DecimalFormat("#,##0.00") @staticmethod def getPercentInstance(locale=None): return DecimalFormat("#,##0%") @staticmethod def getIntegerInstance(locale=None): nf = DecimalFormat("#,##0") nf.setMaximumFractionDigits(0) return nf @staticmethod def getCompactNumberInstance(locale=None, style=None): return DecimalFormat("#,##0") def format(self, number, to_append_to=None, pos=None): raise NotImplementedError def parse(self, source, pos=None): raise NotImplementedError def isGroupingUsed(self): return self._grouping_used def setGroupingUsed(self, v): self._grouping_used = v def getMaximumIntegerDigits(self): return self._max_int_digits def setMaximumIntegerDigits(self, v): self._max_int_digits = v def getMinimumIntegerDigits(self): return self._min_int_digits def setMinimumIntegerDigits(self, v): self._min_int_digits = v def getMaximumFractionDigits(self): return self._max_frac_digits def setMaximumFractionDigits(self, v): self._max_frac_digits = v def getMinimumFractionDigits(self): return self._min_frac_digits def setMinimumFractionDigits(self, v): self._min_frac_digits = v def setParseIntegerOnly(self, v): self._parse_integer_only = v def isParseIntegerOnly(self): return self._parse_integer_only def setRoundingMode(self, v): self._rounding_mode = v def getRoundingMode(self): return self._rounding_mode def getCurrency(self): return "USD" def setCurrency(self, c): pass @staticmethod def getAvailableLocales(): return list(_locale.locale_alias.keys()) # ============================================================ # ChoiceFormat # ============================================================ class ChoiceFormat(NumberFormat): def __init__(self, formats=None, limits=None): super().__init__() self._formats = formats if formats is not None else [] self._limits = limits if limits is not None else [] def format(self, number, to_append_to=None, pos=None): val = float(number) idx = 0 for i, lim in enumerate(self._limits): if val >= lim: idx = i result = str(self._formats[idx]) if idx < len(self._formats) else "" if to_append_to is not None: sb = str(to_append_to) idx2 = len(sb) to_append_to.append(result) if pos is not None: pos.setBeginIndex(idx2) pos.setEndIndex(idx2 + len(result)) return to_append_to return result def parse(self, source, pos=None): raise NotImplementedError @staticmethod def nextDouble(d, upwards=True): if upwards: return d + _py_math.ulp(d) return d - _py_math.ulp(d) @staticmethod def previousDouble(d): return ChoiceFormat.nextDouble(d, False) def getLimits(self): return self._limits def getFormats(self): return self._formats # ============================================================ # MessageFormat # ============================================================ # NumberFormat # ============================================================ # ============================================================ # DecimalFormat # ============================================================ class DecimalFormat(NumberFormat): def __init__(self, pattern=None, symbols=None): super().__init__() self._pattern = pattern or "#,##0.###" self._symbols = symbols or DecimalFormatSymbols() self._decimal_sep_always = False self._grouping_size = 3 self._grouping_used = True self._max_int_digits = 2000000000 self._min_int_digits = 1 self._max_frac_digits = 2000000000 self._min_frac_digits = 0 self._neg_prefix = "-" self._neg_suffix = "" self._pos_prefix = "" self._pos_suffix = "" self._multiplier = 1 self._rounding_mode = 4 self._parse_bigdecimal = False def applyPattern(self, pattern): self._pattern = pattern def toPattern(self): return self._pattern def toLocalizedPattern(self): return self._pattern def applyLocalizedPattern(self, pattern): self._pattern = pattern def format(self, number, to_append_to=None, pos=None): if isinstance(number, (int, float)): val = number * self._multiplier elif isinstance(number, _Decimal): val = float(str(number)) * self._multiplier else: val = float(str(number)) * self._multiplier if self._grouping_used and self._grouping_size > 0: result = f"{val:,.{self._max_frac_digits}f}" else: fmt = "{{:.{}f}}".format(self._max_frac_digits) result = fmt.format(val) prefix = self._pos_prefix if val >= 0 else self._neg_prefix suffix = self._pos_suffix if val >= 0 else self._neg_suffix result = prefix + result + suffix if to_append_to is not None: sb = str(to_append_to) idx = len(sb) to_append_to.append(result) if pos is not None: pos.setBeginIndex(idx) pos.setEndIndex(idx + len(result)) return to_append_to return result def parse(self, source, pos=None): text = source.strip() try: val = float(text) if pos is not None: pos.setIndex(len(source)) if self._parse_bigdecimal: return _Decimal(str(val)) return val except ValueError: if pos is not None: pos.setErrorIndex(0) raise Exception(f"Unparseable: {source}") def setDecimalSeparatorAlwaysShown(self, v): self._decimal_sep_always = v def getDecimalSeparatorAlwaysShown(self): return self._decimal_sep_always def setGroupingSize(self, v): self._grouping_size = v def getGroupingSize(self): return self._grouping_size def setGroupingUsed(self, v): self._grouping_used = v def isGroupingUsed(self): return self._grouping_used def setMaximumIntegerDigits(self, v): self._max_int_digits = v def getMaximumIntegerDigits(self): return self._max_int_digits def setMinimumIntegerDigits(self, v): self._min_int_digits = v def getMinimumIntegerDigits(self): return self._min_int_digits def setMaximumFractionDigits(self, v): self._max_frac_digits = v def getMaximumFractionDigits(self): return self._max_frac_digits def setMinimumFractionDigits(self, v): self._min_frac_digits = v def getMinimumFractionDigits(self): return self._min_frac_digits def setNegativePrefix(self, v): self._neg_prefix = v def getNegativePrefix(self): return self._neg_prefix def setNegativeSuffix(self, v): self._neg_suffix = v def getNegativeSuffix(self): return self._neg_suffix def setPositivePrefix(self, v): self._pos_prefix = v def getPositivePrefix(self): return self._pos_prefix def setPositiveSuffix(self, v): self._pos_suffix = v def getPositiveSuffix(self): return self._pos_suffix def setDecimalFormatSymbols(self, v): self._symbols = v def getDecimalFormatSymbols(self): return self._symbols def setParseBigDecimal(self, v): self._parse_bigdecimal = v def isParseBigDecimal(self): return self._parse_bigdecimal def setMultiplier(self, v): self._multiplier = v def getMultiplier(self): return self._multiplier def setRoundingMode(self, v): self._rounding_mode = v def getRoundingMode(self): return self._rounding_mode # ============================================================ # MessageFormat # ============================================================ _MSG_PATTERN_RE = _re.compile(r"\{(\d+)(?:,(\w+)(?:,(.+))?)?\}") class MessageFormat(Format): def __init__(self, pattern, locale=None): super().__init__() self._pattern = pattern self._locale = locale self._formats = {} def applyPattern(self, pattern): self._pattern = pattern def toPattern(self): return self._pattern def _format_impl(self, objects): def replacer(m): idx = int(m.group(1)) if idx < len(objects): return str(objects[idx]) return m.group(0) return _MSG_PATTERN_RE.sub(replacer, self._pattern) def format(self, objects, to_append_to=None, pos=None): result = self._format_impl(objects) if to_append_to is not None: sb = str(to_append_to) idx = len(sb) to_append_to.append(result) if pos is not None: pos.setBeginIndex(idx) pos.setEndIndex(idx + len(result)) return to_append_to return result @staticmethod def format_static(pattern, *args): mf = MessageFormat(pattern) return mf.format(args) def parse(self, source, pos=None): parts = _MSG_PATTERN_RE.split(self._pattern) result = [] idx = 0 for i, part in enumerate(parts): if i % 2 == 0: if not source[idx:].startswith(part): if pos is not None: pos.setErrorIndex(idx) return result idx += len(part) else: arg_idx = int(part) remaining = source[idx:] next_part = parts[i + 1] if i + 1 < len(parts) else "" end = remaining.find(next_part) if next_part else len(remaining) if end < 0: end = len(remaining) result.append(remaining[:end]) idx += end if pos is not None: pos.setIndex(idx) return result def setLocale(self, locale): self._locale = locale def getLocale(self): return self._locale def setFormats(self, formats): self._formats = formats def getFormats(self): return self._formats def setFormat(self, idx, fmt): self._formats[idx] = fmt def getFormat(self, idx): return self._formats.get(idx) # ============================================================ # CharacterIterator # ============================================================ class CharacterIterator(Stub): DONE = chr(0xFFFF) def first(self): raise NotImplementedError def last(self): raise NotImplementedError def current(self): raise NotImplementedError def next(self): raise NotImplementedError def previous(self): raise NotImplementedError def setIndex(self, position): raise NotImplementedError def getBeginIndex(self): raise NotImplementedError def getEndIndex(self): raise NotImplementedError def getIndex(self): raise NotImplementedError def clone(self): raise NotImplementedError # ============================================================ # StringCharacterIterator # ============================================================ class StringCharacterIterator(CharacterIterator): def __init__(self, text): super().__init__() self._text = text self._begin = 0 self._end = len(text) self._pos = 0 def first(self): self._pos = self._begin return self.current() def last(self): self._pos = self._end - 1 if self._end > self._begin else self._begin return self.current() def current(self): if self._pos >= self._begin and self._pos < self._end: return self._text[self._pos] return CharacterIterator.DONE def next(self): self._pos += 1 if self._pos >= self._end: self._pos = self._end return CharacterIterator.DONE return self._text[self._pos] def previous(self): self._pos -= 1 if self._pos < self._begin: self._pos = self._begin return CharacterIterator.DONE return self._text[self._pos] def setIndex(self, pos): self._pos = max(self._begin, min(pos, self._end)) return self.current() def getBeginIndex(self): return self._begin def getEndIndex(self): return self._end def getIndex(self): return self._pos def clone(self): return StringCharacterIterator(self._text) # ============================================================ # BreakIterator # ============================================================ class BreakIterator(Stub): DONE = -1 @staticmethod def getWordInstance(locale=None): return BreakIterator() @staticmethod def getLineInstance(locale=None): return BreakIterator() @staticmethod def getCharacterInstance(locale=None): return BreakIterator() @staticmethod def getSentenceInstance(locale=None): return BreakIterator() def setText(self, text): self._text = text def first(self): return 0 def last(self): return len(self._text) if hasattr(self, "_text") else 0 def next(self, n=1): return BreakIterator.DONE def previous(self): return BreakIterator.DONE def current(self): return 0 def following(self, offset): return BreakIterator.DONE def preceding(self, offset): return BreakIterator.DONE def isBoundary(self, offset): return True # ============================================================ # Collator / CollationKey / RuleBasedCollator # ============================================================ class CollationKey(Stub): def __init__(self, source, key): super().__init__() self._source = source self._key = key def compareTo(self, other): if isinstance(other, CollationKey): return (self._key > other._key) - (self._key < other._key) return -1 def getSourceString(self): return self._source def toByteArray(self): return self._key.encode("utf-8") class Collator(Stub): PRIMARY = 0 SECONDARY = 1 TERTIARY = 2 IDENTICAL = 3 NO_DECOMPOSITION = 0 CANONICAL_DECOMPOSITION = 1 FULL_DECOMPOSITION = 2 @staticmethod def getInstance(locale=None): return RuleBasedCollator() def compare(self, source, target): return (source > target) - (source < target) def getCollationKey(self, source): return CollationKey(source, source) def setStrength(self, strength): self._strength = strength def getStrength(self): return getattr(self, "_strength", Collator.TERTIARY) def setDecomposition(self, decomposition): self._decomposition = decomposition def getDecomposition(self): return getattr(self, "_decomposition", Collator.NO_DECOMPOSITION) class RuleBasedCollator(Collator): def __init__(self, rules=None): super().__init__() self._rules = rules or "" # ============================================================ # Normalizer # ============================================================ class Normalizer(Stub): class Form(Stub): NFC = "NFC" NFD = "NFD" NFKC = "NFKC" NFKD = "NFKD" @staticmethod def normalize(src, form): import unicodedata form_map = { Normalizer.Form.NFC: "NFC", Normalizer.Form.NFD: "NFD", Normalizer.Form.NFKC: "NFKC", Normalizer.Form.NFKD: "NFKD", } return unicodedata.normalize(form_map.get(form, "NFC"), src) @staticmethod def isNormalized(src, form): return Normalizer.normalize(src, form) == src # ============================================================ # Annotation / AttributedCharacterIterator / AttributedString # ============================================================ class Annotation(Stub): def __init__(self, value): super().__init__() self._value = value def getValue(self): return self._value def toString(self): return str(self._value) class AttributedCharacterIterator(CharacterIterator): pass class AttributedString(Stub): def __init__(self, text, attributes=None): super().__init__() self._text = text def getIterator(self): return StringCharacterIterator(self._text) # ============================================================ # Bidi # ============================================================ class Bidi(Stub): DIRECTION_LEFT_TO_RIGHT = 0 DIRECTION_RIGHT_TO_LEFT = 1 DIRECTION_DEFAULT_LEFT_TO_RIGHT = -2 DIRECTION_DEFAULT_RIGHT_TO_LEFT = -1 def __init__(self, text, flags=0): super().__init__() self._text = text self._base_level = 0 if flags >= 0 else 1 def isLeftToRight(self): return self._base_level == 0 def isRightToLeft(self): return self._base_level == 1 def getLength(self): return len(self._text) def getLevelAt(self, offset): return self._base_level def getRunCount(self): return 1 def getRunLevel(self, run): return self._base_level def getRunLimit(self, run): return len(self._text) def getRunStart(self, run): return 0 def requiresBidi(self, text): for ch in text: if ord(ch) > 0x590: return True return False @staticmethod def reorderVisually(levels, objects, index, count): pass @staticmethod def baseIsLeftToRight(): return True