from typing import Literal, Protocol import regex UNICODE_TO_GSM: dict[int, list[int]] = { 0x000A: [0x0A], 0x000C: [0x1B, 0x0A], 0x000D: [0x0D], 0x0020: [0x20], 0x0021: [0x21], 0x0022: [0x22], 0x0023: [0x23], 0x0024: [0x02], 0x0025: [0x25], 0x0026: [0x26], 0x0027: [0x27], 0x0028: [0x28], 0x0029: [0x29], 0x002A: [0x2A], 0x002B: [0x2B], 0x002C: [0x2C], 0x002D: [0x2D], 0x002E: [0x2E], 0x002F: [0x2F], 0x0030: [0x30], 0x0031: [0x31], 0x0032: [0x32], 0x0033: [0x33], 0x0034: [0x34], 0x0035: [0x35], 0x0036: [0x36], 0x0037: [0x37], 0x0038: [0x38], 0x0039: [0x39], 0x003A: [0x3A], 0x003B: [0x3B], 0x003C: [0x3C], 0x003D: [0x3D], 0x003E: [0x3E], 0x003F: [0x3F], 0x0040: [0x00], 0x0041: [0x41], 0x0042: [0x42], 0x0043: [0x43], 0x0044: [0x44], 0x0045: [0x45], 0x0046: [0x46], 0x0047: [0x47], 0x0048: [0x48], 0x0049: [0x49], 0x004A: [0x4A], 0x004B: [0x4B], 0x004C: [0x4C], 0x004D: [0x4D], 0x004E: [0x4E], 0x004F: [0x4F], 0x0050: [0x50], 0x0051: [0x51], 0x0052: [0x52], 0x0053: [0x53], 0x0054: [0x54], 0x0055: [0x55], 0x0056: [0x56], 0x0057: [0x57], 0x0058: [0x58], 0x0059: [0x59], 0x005A: [0x5A], 0x005B: [0x1B, 0x3C], 0x005C: [0x1B, 0x2F], 0x005D: [0x1B, 0x3E], 0x005E: [0x1B, 0x14], 0x005F: [0x11], 0x0061: [0x61], 0x0062: [0x62], 0x0063: [0x63], 0x0064: [0x64], 0x0065: [0x65], 0x0066: [0x66], 0x0067: [0x67], 0x0068: [0x68], 0x0069: [0x69], 0x006A: [0x6A], 0x006B: [0x6B], 0x006C: [0x6C], 0x006D: [0x6D], 0x006E: [0x6E], 0x006F: [0x6F], 0x0070: [0x70], 0x0071: [0x71], 0x0072: [0x72], 0x0073: [0x73], 0x0074: [0x74], 0x0075: [0x75], 0x0076: [0x76], 0x0077: [0x77], 0x0078: [0x78], 0x0079: [0x79], 0x007A: [0x7A], 0x007B: [0x1B, 0x28], 0x007C: [0x1B, 0x40], 0x007D: [0x1B, 0x29], 0x007E: [0x1B, 0x3D], 0x00A1: [0x40], 0x00A3: [0x01], 0x00A4: [0x24], 0x00A5: [0x03], 0x00A7: [0x5F], 0x00BF: [0x60], 0x00C4: [0x5B], 0x00C5: [0x0E], 0x00C6: [0x1C], 0x00C9: [0x1F], 0x00D1: [0x5D], 0x00D6: [0x5C], 0x00D8: [0x0B], 0x00DC: [0x5E], 0x00DF: [0x1E], 0x00E0: [0x7F], 0x00E4: [0x7B], 0x00E5: [0x0F], 0x00E6: [0x1D], 0x00C7: [0x09], 0x00E8: [0x04], 0x00E9: [0x05], 0x00EC: [0x07], 0x00F1: [0x7D], 0x00F2: [0x08], 0x00F6: [0x7C], 0x00F8: [0x0C], 0x00F9: [0x06], 0x00FC: [0x7E], 0x0393: [0x13], 0x0394: [0x10], 0x0398: [0x19], 0x039B: [0x14], 0x039E: [0x1A], 0x03A0: [0x16], 0x03A3: [0x18], 0x03A6: [0x12], 0x03A8: [0x17], 0x03A9: [0x15], 0x20AC: [0x1B, 0x65], } SMART_ENCODING_MAP: dict[str, str] = { "\u00ab": '"', # LEFT-POINTING DOUBLE ANGLE QUOTATION MARK "\u00bb": '"', # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK "\u201c": '"', # LEFT DOUBLE QUOTATION MARK "\u201d": '"', # RIGHT DOUBLE QUOTATION MARK "\u02ba": '"', # MODIFIER LETTER DOUBLE PRIME "\u02ee": '"', # MODIFIER LETTER DOUBLE APOSTROPHE "\u201f": '"', # DOUBLE HIGH-REVERSED-9 QUOTATION MARK "\u201e": '"', # DOUBLE LOW-9 QUOTATION MARK "\u275d": '"', # HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT "\u275e": '"', # HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT "\u301d": '"', # REVERSED DOUBLE PRIME QUOTATION MARK "\u301e": '"', # DOUBLE PRIME QUOTATION MARK "\uff02": '"', # FULLWIDTH QUOTATION MARK "\u2018": "'", # LEFT SINGLE QUOTATION MARK "\u2019": "'", # RIGHT SINGLE QUOTATION MARK "\u02bb": "'", # MODIFIER LETTER TURNED COMMA "\u02c8": "'", # MODIFIER LETTER VERTICAL LINE "\u02bc": "'", # MODIFIER LETTER APOSTROPHE "\u02bd": "'", # MODIFIER LETTER REVERSED COMMA "\u02b9": "'", # MODIFIER LETTER PRIME "\u201b": "'", # SINGLE HIGH-REVERSED-9 QUOTATION MARK "\uff07": "'", # FULLWIDTH APOSTROPHE "\u00b4": "'", # ACUTE ACCENT "\u02ca": "'", # MODIFIER LETTER ACUTE ACCENT "\u0060": "'", # GRAVE ACCENT "\u02cb": "'", # MODIFIER LETTER GRAVE ACCENT "\u275b": "'", # HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT "\u275c": "'", # HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT "\u0313": "'", # COMBINING COMMA ABOVE "\u0314": "'", # COMBINING REVERSED COMMA ABOVE "\ufe10": "'", # PRESENTATION FORM FOR VERTICAL COMMA "\ufe11": "'", # PRESENTATION FORM FOR VERTICAL IDEOGRAPHIC COMMA "\u00f7": "/", # DIVISION SIGN "\u00bc": "1/4", # VULGAR FRACTION ONE QUARTER "\u00bd": "1/2", # VULGAR FRACTION ONE HALF "\u00be": "3/4", # VULGAR FRACTION THREE QUARTERS "\u29f8": "/", # BIG SOLIDUS "\u0337": "/", # COMBINING SHORT SOLIDUS OVERLAY "\u0338": "/", # COMBINING LONG SOLIDUS OVERLAY "\u2044": "/", # FRACTION SLASH "\u2215": "/", # DIVISION SLASH "\uff0f": "/", # FULLWIDTH SOLIDUS "\u29f9": "\\", # BIG REVERSE SOLIDUS "\u29f5": "\\", # REVERSE SOLIDUS OPERATOR "\u20e5": "\\", # COMBINING REVERSE SOLIDUS OVERLAY "\ufe68": "\\", # SMALL REVERSE SOLIDUS "\uff3c": "\\", # FULLWIDTH REVERSE SOLIDUS "\u0332": "_", # COMBINING LOW LINE "\uff3f": "_", # FULLWIDTH LOW LINE "\u20d2": "|", # COMBINING LONG VERTICAL LINE OVERLAY "\u20d3": "|", # COMBINING SHORT VERTICAL LINE OVERLAY "\u2223": "|", # DIVIDES "\uff5c": "|", # FULLWIDTH VERTICAL LINE "\u23b8": "|", # LEFT VERTICAL BOX LINE "\u23b9": "|", # RIGHT VERTICAL BOX LINE "\u23d0": "|", # VERTICAL LINE EXTENSION "\u239c": "|", # LEFT PARENTHESIS EXTENSION "\u239f": "|", # RIGHT PARENTHESIS EXTENSION "\u23bc": "-", # HORIZONTAL SCAN LINE-7 "\u23bd": "-", # HORIZONTAL SCAN LINE-9 "\u2015": "-", # HORIZONTAL BAR "\ufe63": "-", # SMALL HYPHEN-MINUS "\uff0d": "-", # FULLWIDTH HYPHEN-MINUS "\u2010": "-", # HYPHEN "\u2043": "-", # HYPHEN BULLET "\ufe6b": "@", # SMALL COMMERCIAL AT "\uff20": "@", # FULLWIDTH COMMERCIAL AT "\ufe69": "$", # SMALL DOLLAR SIGN "\uff04": "$", # FULLWIDTH DOLLAR SIGN "\u01c3": "!", # LATIN LETTER RETROFLEX CLICK "\ufe15": "!", # PRESENTATION FORM FOR VERTICAL EXLAMATION MARK "\ufe57": "!", # SMALL EXCLAMATION MARK "\uff01": "!", # FULLWIDTH EXCLAMATION MARK "\ufe5f": "#", # SMALL NUMBER SIGN "\uff03": "#", # FULLWIDTH NUMBER SIGN "\ufe6a": "%", # SMALL PERCENT SIGN "\uff05": "%", # FULLWIDTH PERCENT SIGN "\ufe60": "&", # SMALL AMPERSAND "\uff06": "&", # FULLWIDTH AMPERSAND "\u201a": ",", # SINGLE LOW-9 QUOTATION MARK "\u0326": ",", # COMBINING COMMA BELOW "\ufe50": ",", # SMALL COMMA "\ufe51": ",", # SMALL IDEOGRAPHIC COMMA "\uff0c": ",", # FULLWIDTH COMMA "\uff64": ",", # HALFWIDTH IDEOGRAPHIC COMMA "\u2768": "(", # MEDIUM LEFT PARENTHESIS ORNAMENT "\u276a": "(", # MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT "\ufe59": "(", # SMALL LEFT PARENTHESIS "\uff08": "(", # FULLWIDTH LEFT PARENTHESIS "\u27ee": "(", # MATHEMATICAL LEFT FLATTENED PARENTHESIS "\u2985": "(", # LEFT WHITE PARENTHESIS "\u2769": ")", # MEDIUM RIGHT PARENTHESIS ORNAMENT "\u276b": ")", # MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT "\ufe5a": ")", # SMALL RIGHT PARENTHESIS "\uff09": ")", # FULLWIDTH RIGHT PARENTHESIS "\u27ef": ")", # MATHEMATICAL RIGHT FLATTENED PARENTHESIS "\u2986": ")", # RIGHT WHITE PARENTHESIS "\u204e": "*", # LOW ASTERISK "\u2217": "*", # ASTERISK OPERATOR "\u229b": "*", # CIRCLED ASTERISK OPERATOR "\u2722": "*", # FOUR TEARDROP-SPOKED ASTERISK "\u2723": "*", # FOUR BALLOON-SPOKED ASTERISK "\u2724": "*", # HEAVY FOUR BALLOON-SPOKED ASTERISK "\u2725": "*", # FOUR CLUB-SPOKED ASTERISK "\u2731": "*", # HEAVY ASTERISK "\u2732": "*", # OPEN CENTRE ASTERISK "\u2733": "*", # EIGHT SPOKED ASTERISK "\u273a": "*", # SIXTEEN POINTED ASTERISK "\u273b": "*", # TEARDROP-SPOKED ASTERISK "\u273c": "*", # OPEN CENTRE TEARDROP-SPOKED ASTERISK "\u273d": "*", # HEAVY TEARDROP-SPOKED ASTERISK "\u2743": "*", # HEAVY TEARDROP-SPOKED PINWHEEL ASTERISK "\u2749": "*", # BALLOON-SPOKED ASTERISK "\u274a": "*", # EIGHT TEARDROP-SPOKED PROPELLER ASTERISK "\u274b": "*", # HEAVY EIGHT TEARDROP-SPOKED PROPELLER ASTERISK "\u29c6": "*", # SQUARED ASTERISK "\ufe61": "*", # SMALL ASTERISK "\uff0a": "*", # FULLWIDTH ASTERISK "\u02d6": "+", # MODIFIER LETTER PLUS SIGN "\ufe62": "+", # SMALL PLUS SIGN "\uff0b": "+", # FULLWIDTH PLUS SIGN "\u3002": ".", # IDEOGRAPHIC FULL STOP "\ufe52": ".", # SMALL FULL STOP "\uff0e": ".", # FULLWIDTH FULL STOP "\uff61": ".", # HALFWIDTH IDEOGRAPHIC FULL STOP "\uff10": "0", # FULLWIDTH DIGIT ZERO "\uff11": "1", # FULLWIDTH DIGIT ONE "\uff12": "2", # FULLWIDTH DIGIT TWO "\uff13": "3", # FULLWIDTH DIGIT THREE "\uff14": "4", # FULLWIDTH DIGIT FOUR "\uff15": "5", # FULLWIDTH DIGIT FIVE "\uff16": "6", # FULLWIDTH DIGIT SIX "\uff17": "7", # FULLWIDTH DIGIT SEVEN "\uff18": "8", # FULLWIDTH DIGIT EIGHT "\uff19": "9", # FULLWIDTH DIGIT NINE "\u02d0": ":", # MODIFIER LETTER TRIANGULAR COLON "\u02f8": ":", # MODIFIER LETTER RAISED COLON "\u2982": ":", # Z NOTATION TYPE COLON "\ua789": ":", # MODIFIER LETTER COLON "\ufe13": ":", # PRESENTATION FORM FOR VERTICAL COLON "\uff1a": ":", # FULLWIDTH COLON "\u204f": ";", # REVERSED SEMICOLON "\ufe14": ";", # PRESENTATION FORM FOR VERTICAL SEMICOLON "\ufe54": ";", # SMALL SEMICOLON "\uff1b": ";", # FULLWIDTH SEMICOLON "\ufe64": "<", # SMALL LESS-THAN SIGN "\uff1c": "<", # FULLWIDTH LESS-THAN SIGN "\u0347": "=", # COMBINING EQUALS SIGN BELOW "\ua78a": "=", # MODIFIER LETTER SHORT EQUALS SIGN "\ufe66": "=", # SMALL EQUALS SIGN "\uff1d": "=", # FULLWIDTH EQUALS SIGN "\ufe65": ">", # SMALL GREATER-THAN SIGN "\uff1e": ">", # FULLWIDTH GREATER-THAN SIGN "\ufe16": "?", # PRESENTATION FORM FOR VERTICAL QUESTION MARK "\ufe56": "?", # SMALL QUESTION MARK "\uff1f": "?", # FULLWIDTH QUESTION MARK "\uff21": "A", # FULLWIDTH LATIN CAPITAL LETTER A "\u1d00": "A", # LATIN LETTER SMALL CAPITAL A "\uff22": "B", # FULLWIDTH LATIN CAPITAL LETTER B "\u0299": "B", # LATIN LETTER SMALL CAPITAL B "\uff23": "C", # FULLWIDTH LATIN CAPITAL LETTER C "\u1d04": "C", # LATIN LETTER SMALL CAPITAL C "\uff24": "D", # FULLWIDTH LATIN CAPITAL LETTER D "\u1d05": "D", # LATIN LETTER SMALL CAPITAL D "\uff25": "E", # FULLWIDTH LATIN CAPITAL LETTER E "\u1d07": "E", # LATIN LETTER SMALL CAPITAL E "\uff26": "F", # FULLWIDTH LATIN CAPITAL LETTER F "\ua730": "F", # LATIN LETTER SMALL CAPITAL F "\uff27": "G", # FULLWIDTH LATIN CAPITAL LETTER G "\u0262": "G", # LATIN LETTER SMALL CAPITAL G "\uff28": "H", # FULLWIDTH LATIN CAPITAL LETTER H "\u029c": "H", # LATIN LETTER SMALL CAPITAL H "\uff29": "I", # FULLWIDTH LATIN CAPITAL LETTER I "\u026a": "I", # LATIN LETTER SMALL CAPITAL I "\uff2a": "J", # FULLWIDTH LATIN CAPITAL LETTER J "\u1d0a": "J", # LATIN LETTER SMALL CAPITAL J "\uff2b": "K", # FULLWIDTH LATIN CAPITAL LETTER K "\u1d0b": "K", # LATIN LETTER SMALL CAPITAL K "\uff2c": "L", # FULLWIDTH LATIN CAPITAL LETTER L "\u029f": "L", # LATIN LETTER SMALL CAPITAL L "\uff2d": "M", # FULLWIDTH LATIN CAPITAL LETTER M "\u1d0d": "M", # LATIN LETTER SMALL CAPITAL M "\uff2e": "N", # FULLWIDTH LATIN CAPITAL LETTER N "\u0274": "N", # LATIN LETTER SMALL CAPITAL N "\uff2f": "O", # FULLWIDTH LATIN CAPITAL LETTER O "\u1d0f": "O", # LATIN LETTER SMALL CAPITAL O "\uff30": "P", # FULLWIDTH LATIN CAPITAL LETTER P "\u1d18": "P", # LATIN LETTER SMALL CAPITAL P "\uff31": "Q", # FULLWIDTH LATIN CAPITAL LETTER Q "\uff32": "R", # FULLWIDTH LATIN CAPITAL LETTER R "\u0280": "R", # LATIN LETTER SMALL CAPITAL R "\uff33": "S", # FULLWIDTH LATIN CAPITAL LETTER S "\ua731": "S", # LATIN LETTER SMALL CAPITAL S "\uff34": "T", # FULLWIDTH LATIN CAPITAL LETTER T "\u1d1b": "T", # LATIN LETTER SMALL CAPITAL T "\uff35": "U", # FULLWIDTH LATIN CAPITAL LETTER U "\u1d1c": "U", # LATIN LETTER SMALL CAPITAL U "\uff36": "V", # FULLWIDTH LATIN CAPITAL LETTER V "\u1d20": "V", # LATIN LETTER SMALL CAPITAL V "\uff37": "W", # FULLWIDTH LATIN CAPITAL LETTER W "\u1d21": "W", # LATIN LETTER SMALL CAPITAL W "\uff38": "X", # FULLWIDTH LATIN CAPITAL LETTER X "\uff39": "Y", # FULLWIDTH LATIN CAPITAL LETTER Y "\u028f": "Y", # LATIN LETTER SMALL CAPITAL Y "\uff3a": "Z", # FULLWIDTH LATIN CAPITAL LETTER Z "\u1d22": "Z", # LATIN LETTER SMALL CAPITAL Z "\u02c6": "^", # MODIFIER LETTER CIRCUMFLEX ACCENT "\u0302": "^", # COMBINING CIRCUMFLEX ACCENT "\uff3e": "^", # FULLWIDTH CIRCUMFLEX ACCENT "\u1dcd": "^", # COMBINING DOUBLE CIRCUMFLEX ABOVE "\u2774": "{", # MEDIUM LEFT CURLY BRACKET ORNAMENT "\ufe5b": "{", # SMALL LEFT CURLY BRACKET "\uff5b": "{", # FULLWIDTH LEFT CURLY BRACKET "\u2775": "}", # MEDIUM RIGHT CURLY BRACKET ORNAMENT "\ufe5c": "}", # SMALL RIGHT CURLY BRACKET "\uff5d": "}", # FULLWIDTH RIGHT CURLY BRACKET "\uff3b": "[", # FULLWIDTH LEFT SQUARE BRACKET "\uff3d": "]", # FULLWIDTH RIGHT SQUARE BRACKET "\u02dc": "~", # SMALL TILDE "\u02f7": "~", # MODIFIER LETTER LOW TILDE "\u0303": "~", # COMBINING TILDE "\u0330": "~", # COMBINING TILDE BELOW "\u0334": "~", # COMBINING TILDE OVERLAY "\u223c": "~", # TILDE OPERATOR "\uff5e": "~", # FULLWIDTH TILDE "\u00a0": " ", # NO-BREAK SPACE "\u2000": " ", # EN QUAD "\u2002": " ", # EN SPACE "\u2003": " ", # EM SPACE "\u2004": " ", # THREE-PER-EM SPACE "\u2005": " ", # FOUR-PER-EM SPACE "\u2006": " ", # SIX-PER-EM SPACE "\u2007": " ", # FIGURE SPACE "\u2008": " ", # PUNCTUATION SPACE "\u2009": " ", # THIN SPACE "\u200a": " ", # HAIR SPACE "\u202f": " ", # NARROW NO-BREAK SPACE "\u205f": " ", # MEDIUM MATHEMATICAL SPACE "\u3000": " ", # IDEOGRAHPIC SPACE "\u008d": " ", # REVERSE LINE FEED (standard LF looks like \n, this looks like a space) "\u009f": " ", # "\u0080": " ", # C1 CONTROL CODES "\u0090": " ", # DEVICE CONTROL STRING "\u009b": " ", # CONTROL SEQUENCE INTRODUCER "\u0010": "", # ESCAPE, DATA LINK (not visible) "\u0009": " ", # TAB (7 spaces based on print statement in Python interpreter) "\u0000": "", # NULL "\u0003": "", # END OF TEXT "\u0004": "", # END OF TRANSMISSION "\u0017": "", # END OF TRANSMISSION BLOCK "\u0019": "", # END OF MEDIUM "\u0011": "", # DEVICE CONTROL ONE "\u0012": "", # DEVICE CONTROL TWO "\u0013": "", # DEVICE CONTROL THREE "\u0014": "", # DEVICE CONTROL FOUR "\u2060": "", # WORD JOINER "\u2017": "_", # DOUBLE LOW LINE "\u2014": "-", # EM DASH "\u2013": "-", # EN DASH "\u2039": ">", # Single left-pointing angle quotation mark "\u203a": "<", # Single right-pointing angle quotation mark "\u203c": "!!", # Double exclamation mark "\u2028": " ", # Whitespace: Line Separator "\u2029": " ", # Whitespace: Paragraph Separator "\u2026": "...", # Whitespace: Narrow No-Break Space "\u2001": " ", # Whitespace: Medium Mathematical Space "\u200b": "", # ZERO WIDTH SPACE "\u3001": ",", # IDEOGRAPHIC COMMA "\ufeff": "", # ZERO WIDTH NO-BREAK SPACE "\u2022": "-", # Bullet } Encoding = Literal["GSM-7", "UCS-2"] def to_utf16_code_units(text: str) -> list[int]: # Encode to UTF-16 big endian, skip BOM utf16_bytes = text.encode("utf-16-be") return [ (utf16_bytes[i] << 8) | utf16_bytes[i + 1] for i in range(0, len(utf16_bytes), 2) ] class Encodable(Protocol): @property def code_unit_size_in_bits(self) -> int: ... @property def size_in_bits(self) -> int: ... class EncodedChar: def __init__(self, char: str, encoding: Encoding): self.raw = char self.encoding = encoding self.is_GSM7 = bool(char and len(char) == 1 and ord(char) in UNICODE_TO_GSM) if self.is_GSM7 and encoding == "GSM-7": self.code_units = UNICODE_TO_GSM[ord(char)] elif encoding == "UCS-2": self.code_units = to_utf16_code_units(char) else: self.code_units = [ord(c) for c in char] @property def code_unit_size_in_bits(self) -> int: return 7 if self.encoding == "GSM-7" else 8 @property def size_in_bits(self) -> int: if self.encoding == "UCS-2" and self.is_GSM7: return 16 bits_per_unit = 7 if self.encoding == "GSM-7" else 16 return bits_per_unit * len(self.code_units) class UserDataHeader: @property def code_unit_size_in_bits(self) -> int: return 8 @property def size_in_bits(self) -> int: return 8 class Segment(list[Encodable]): def __init__(self, *, with_user_data_header: bool = False): super().__init__() self.has_twilio_reserved_bits = with_user_data_header self.has_user_data_header = with_user_data_header if with_user_data_header: for _ in range(6): self.append(UserDataHeader()) @property def size_in_bits(self) -> int: return sum(char.size_in_bits for char in self) @property def message_size_in_bits(self) -> int: return sum( char.size_in_bits if not isinstance(char, UserDataHeader) else 0 for char in self ) @property def free_size_in_bits(self) -> int: max_bits_in_segment = 140 * 8 # 1120 bits return max_bits_in_segment - self.size_in_bits def add_header(self) -> list[Encodable]: if self.has_user_data_header: return [] left_over_chars: list[Encodable] = [] self.has_twilio_reserved_bits = True self.has_user_data_header = False for _ in range(6): self.insert(0, UserDataHeader()) while self.free_size_in_bits < 0: left_over_chars.insert(0, self.pop()) return left_over_chars class SegmentedMessage: def __init__( self, message: str, encoding: Encoding | Literal["auto"] = "auto", smart_encoding: bool = False, ): if smart_encoding: message = "".join([SMART_ENCODING_MAP.get(c, c) for c in message]) self.graphemes = self._split_graphemes(message) self.number_of_unicode_scalars = len(message) self._encoding = encoding if encoding == "auto": self.encoding: Encoding = ( "UCS-2" if self._has_any_ucs_characters(self.graphemes) else "GSM-7" ) else: if encoding == "GSM-7" and self._has_any_ucs_characters(self.graphemes): raise ValueError( "The string provided is incompatible with GSM-7 encoding" ) self.encoding = encoding self.encoded_chars = self._encode_chars(self.graphemes) self.number_of_characters = ( len(self.graphemes) if self.encoding == "UCS-2" else self._count_code_units(self.encoded_chars) ) self.segments = self._build_segments(self.encoded_chars) self.line_break_style = self._detect_line_break_style(message) self.warnings = self._check_for_warnings() @property def total_size(self) -> int: return sum(segment.size_in_bits for segment in self.segments) @property def message_size(self) -> int: return sum(segment.message_size_in_bits for segment in self.segments) @property def segments_count(self) -> int: return len(self.segments) @property def non_gsm_characters(self) -> list[str]: return [ec.raw for ec in self.encoded_chars if not ec.is_GSM7] @staticmethod def _split_graphemes(message: str) -> list[str]: graphemes = regex.findall(r"\X", message) result: list[str] = [] for g in graphemes: if g == "\r\n": result.extend(list(g)) else: result.append(g) return result @staticmethod def _has_any_ucs_characters(graphemes: list[str]) -> bool: for g in graphemes: if len(g) >= 2 or (len(g) == 1 and ord(g) not in UNICODE_TO_GSM): return True return False def _encode_chars(self, graphemes: list[str]) -> list[EncodedChar]: return [EncodedChar(g, self.encoding) for g in graphemes] @staticmethod def _count_code_units(encoded_chars: list[EncodedChar]) -> int: return sum(len(ec.code_units) for ec in encoded_chars) @staticmethod def _build_segments(encoded_chars: list[EncodedChar]) -> list[Segment]: segments = [Segment()] current_segment = segments[0] for ec in encoded_chars: if current_segment.free_size_in_bits < ec.size_in_bits: segments.append(Segment(with_user_data_header=True)) current_segment = segments[-1] previous = segments[-2] if not previous.has_user_data_header: removed = previous.add_header() current_segment.extend(removed) current_segment.append(ec) return segments @staticmethod def _detect_line_break_style(message: str) -> str | None: has_crlf = "\r\n" in message has_lf = "\n" in message if not has_crlf and not has_lf: return None if has_crlf and has_lf: return "LF+CRLF" return "CRLF" if has_crlf else "LF" def _check_for_warnings(self) -> list[str]: warnings: list[str] = [] if self.line_break_style: warnings.append( "The message has line breaks; the web utility only supports LF style. " "CRLF will be converted to LF." ) return warnings