Плаваюча (рухома) крапка, частина 10. Двійковий експорт-імпорт. Зовнішні машинні транспорти

Частини 1-2 — разом з каталогом всіх частин.

Загальний принцип і окремі представлення

Все попереднє, крім текстового експорту-імпорту (частина 5), стосувалось різних внутрішніх представлень і використань. Ще одна поки що не охоплена тема — це зовнішні представлення, які явно оптимізуються на використання компʼютерного зберігання і обміну даними, а не для використання (запису, читання) людиною. Тут треба говорити як про представлення окремого значення, так і участі в більших структурах, де це тільки одне дане серед багатьох.

Конкретне представлення може бути текстовим, напівтекстовим (принципи двійкового представлення при обмеженні сумісности з текстом) і двійковим. Не вдаватимемося в їх відмінности, це окрема тема на тройку сторінок, залишимо тут для інтуїтивного розуміння. Для текстової передачі останні рокі, вважаємо, нема нічого крім Unicode в представленні UTF-8, проте, тут нам достатньо ASCII; різна локальна екзотика, як EBCDIC, все одно не перешкодить. Але, що критично, локалізовані/реґіональні формати (див. частину 5 про локалі/культури) неприпустимі в принципі. Для двійкового представлення, майже всі передачі по мережі, як Internet, чи зовнішні транспорти навіть до найближчого диску, використовують 8-бітні порції даних. Для більшости IT спеціалістів зараз «байт» означає тільки ґрупу з 8 біт. В контексті всередині звичайного компʼютера чи телефона це працює. Але для зовнішнього транспорта треба уточняти, бо «байт» тут буває від 5 до 16 біт (і це, мабуть, я ще не все бачив;) ) Щоб не було проблем, деякі стандарти (переважно ISO) використовують термін «октет» для того, що завжди 8 біт. Далі, якщо не уточнено, у нас ASCII, і/або байт ≡ октет ≡ ґрупа з 8бітів.

Мабуть, найменше, що тут з проблем такої передачі, це порядок байтів (вікі). Нагадаю: передаючи, наприклад, число π, маємо два базових варіанти big-endian і little-endian (не перекладатиму, бо немає усталених варіантів; джерело термінів з докладним описом всіх аспектів):

Спочатку для цілого — для порівняння:

>>> binascii.hexlify(struct.pack('>h', 0x190), ' ')
b'01 90'
>>> binascii.hexlify(struct.pack('<h', 0x190), ' ')
b'90 01'

А тепер для плаваючого — як воно може бути представлено у найтиповішому на зараз варіанті:

>>> binascii.hexlify(struct.pack('>d', math.pi), ' ')
b'40 09 21 fb 54 44 2d 18'
>>> binascii.hexlify(struct.pack('<d', math.pi), ' ')
b'18 2d 44 54 fb 21 09 40'

Крім порядку байт, все інше відповідає транспортному представленню згідно стандарту, і, у більшості платформ, внутрішньому бітовому представленню. Звісно, розмір може бути різним (тут було 8 байт, може бути 2, 4, 10, 16, такі формати згадувались). Більшість мережевих транспортів зараз вимагала big-endian, зараз це складно стверджувати однозначно. Локальні файлові формати можуть бути якими завгодно, проте вимоги сортування (див. частину 6) теж приводять до big-endian. Переважаюча доля little-endian платформ на зараз має більш історичні причини, ніж раціональні; тому не залагаємось на це, але памʼятаємо.

В деяких форматах є заповнення хвосту (padding), яке приводить до більшого розміру; у більшості Unix-like платформ 10-байтний «extended» x86 зберігається в памʼяті як 12- чи 16-байтне поле, в залежності від вибраного ABI і опцій компіляції. Це теж треба враховувати. Наприклад, пітонівський numpy має, формально, тип float128. Але якщо хтось подивиться всередину на його реалізаціях на Ubuntu/x86-64 чи схожій платформі, побачить 10-байтний «extended», хоча tobytes() видає таки 16 байт 😱

In [1]: import numpy as np
In [2]: c0 = np.float128(0); list(c0.tobytes())
Out[2]: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 137, 253, 127, 0, 0]
In [3]: c0 = np.float128(1); list(c0.tobytes())
Out[3]: [0, 0, 0, 0, 0, 0, 0, 128, 255, 63, 23, 137, 253, 127, 0, 0]
In [4]: c0 = np.float128(2); list(c0.tobytes())
Out[4]: [0, 0, 0, 0, 0, 0, 0, 128, 0, 64, 23, 137, 253, 127, 0, 0]

цей хвіст 23, 137, 253, 127, 0, 0 в тесті був присутній і однаковий для будь-якого числа в numpy.float128... але при перезапуску інтерпретатора Python замінився на 240, 247, 254, 127, 0, 0; при іншому старті — 220, 194, 255, 127, 0, 0... Очевидно, він частково випадковий. Порівнювати на рівність у таких даних можна тільки перші реальні, тобто 10, байт. Це проблема, якщо хтось наївно буде порівнювати всю формальну довжину значення... 😢 (Такий же ефект, згадую, був зі struct sockaddr_in на FreeBSD приблизно до версії 4.0: без попереднього заповнення всеї структури нульовими байтами можна було отримати містичні помилки.)

Якщо хтось бачить платформу на x86, де numpy.float128 це повноцінне плаваюче четверної точности, йому дуже пощастило. Я з таким не зіткнувся. А ось на AArch64 (ARM/64) таки маємо нормальну 128-бітну рухому крапку: (Amazon Linux 2023 останній)

>>> numpy.frombuffer(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@', dtype = numpy.float128)[0]
2.0
>>> numpy.frombuffer(b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@', dtype = numpy.float128)[0]
2.0000000000000000000000000000000004

Такі «фокуси» треба мати на увазі.

Взагалі, якщо подивитись на вирішення роботи з такими представленнями в різних мовах програмування, можна помітити дуже серйозні проблеми.

Візьмемо C або C++. Ось у нас є значення в double. Нам треба його розмістити в виходному буфері. Ми знаємо, теоретично, що double це 64-бітний двійковий IEEE754. Ми знаємо, теоретично, що байт у нас 8-бітний. Але це теоретично. Готуймось зустріти цілий букет проблем:

1) Ось я напишу, наприклад, *(double*)(pos) = the_value;, що може піти не так? Той, хто знає, одразу збудиться: Aliasing! Ми не можемо так робити, бо компілятор може переставити це копіювання до чи після іншої операції з цими даними, включаючи відправку в файл чи сокет. Переробляємо на те, як вимагають сучасні компілятори: memcpy(pos, val, sizeof(val));. Сучасні компілятори бачать таку memcpy і перетворюють на одну операцію запису з регістра. Древні (які ще бувають), навпаки, викликають тут явну бібліотечну memcpy... з ними краще було б як раз робити записом по вказівнику. Писатимемо з #ifʼами? Зупинюсь тут, по аліасінґу (слова: C strict aliasing) достатньо ресурсів в Internet.

2) Хто ґарантував 8-бітний байт і відповідність 1:1 того що в памʼяті побайтно — тому, що буде в сокеті? Добре, перше, вважаємо, на будь-якій платформі, з якою маємо справу (і то, краще було б в коді поставити #error у випадку CHAR_BIT != 8). Друге — взагалі не питання мови, а питання бібліотеки. Ну вважаймо, що виконано.

3) А хто ґарантує, що при виконанні операцій за IEEE754 представлення у конкретного процесора і рантайма в памʼяті відповідатиме «транспортному» формату? Впевнені? Тоді дивіться вище про 6-байтне рандомне сміття в представленні нібито float128 (по факту float80) в numpy. В C ви можете отримати sizeof(long double) == 16, бо вирівнювання і паддінґ сміттям. Це ще й секьюріті проблема, через той ненульовий паддінґ можуть утікати критичні дані.

4) А порядок байт визначатиме хто? Пушкін, Котляревський, абож Басьо? Big-endian, little-endian, інше?

Насправді переважна більшість тих, хто пише код на мовах рівня C, C++, не замислюються над цими питаннями (крім порядку байт, на нього натрапити простіше за все), вважаючи їх автомаґічно вирішеними. Ті, хто вже стикався з потенціальними проблемами, скоріш за все, писатимуть як завжди, але обкладуть перевірками часу компіляції чи тестування: якщо експорт, наприклад, +2.0 не виглядає послідовністю 64, 0, 0, 0 — test failed і ідемо шукати, що не так.

Існує ISO/IEC TS 18661, в яких, наприклад, в частині 3 оголошені типи decencodingN_t і binencodingN_t, де N — бітовий розмір значення, а в частині 2 функції як {encode,decode}{bin,dec}d{32,64,128}, але вже з типами, як _Decimal{32,64,128}. Але і там порядок байтів «implementation-defined». Стандарт прийнятий і його можна помацати (за суттєві гроші), але в межах свого персонального акваріуму. Живої реалізації повного такого експорту-імпорту я не бачив.

Добре, коли мова вже ґарантує відповідність стандартним очікуванням. Якщо у вас Java, ByteBuffer bb; ... bb.order(ByteOrder.BIG_ENDIAN); bb.putFloat(the_value);, і можна не думати про інші варіанти. Те ж саме для JavaScript, PHP, Go, Erlang, десятки їх. Але на нижньому рівні, все одно, хтось має вирішити ці проблеми, і надати стабільний інтерфейс.

І необмежено структурно...

А ще окрема велика тема — структурні метаформати, і тут є багато дивного, тому решта частини буде присвʼячена саме їм.

Структурний метаформат (далі: СМФ) — спосіб представлення даних довільної складности, при якому точна структура задається кодом, але збирається вона зі стандартизованих елементарних значень стандартними підходами ґрупування. Звучить трохи пафосно, але приклади зараз, мабуть, бачив кожний, хто в галузі: вже JSON відповідає цьому означенню, з його елементарними типами (число/рядок/булевське/null) і ґрупуваннями («масив» (cписок) і «обʼєкт» (мапа)). Визначити базові елементи і як їх обʼєднувати в більші ґрупи — і базовий підхід готовий. Конкретний формат, зазвичай, задається «схемою» (schema), яка виставляє вимоги і обмеження. Це окремий величезний світ, з сумішшю близьких підходів, який використовується зараз майже всюди. Але це тема для окремого обговорення; зараз вважаємо, що загальна ідея вже зрозуміла, і концентруємось на передачі саме значень рухомої крапки.

Для значень рухомої крапки найпростіше, мабуть, побачити, це коли формат дозволяє тільки одне (як f64, тобто IEEE binfloat64) чи два формати (f32, f64); в таких все просто і прямолінійно, говорити майже нема про що. Ті структурні метаформати, що розроблені «ad hoc» для відповідности «злобі дня», саме так і зроблені. Але можуть бути і більш цікаві варіанти, які зарані підтоговлені до значно менш фіксованих використань і обмежень.

Почнемо з текстових представлень, після них — бінарні, в підґрупах в порядку зменшення розповсюджености (звісно, приблизно). Не всі формати покриємо, їх декілька десятків, але базові підходи, думаю, всі.

JSON

Мабуть, найрозповсюдженіше з того, що типовий читач даного циклу міг бачити. Є універсальний тип number, який обʼєднує цілі і плаваючі. Представлення значення — тільки текстове і десяткове. Тобто, π буде записано як 3.141592653589793, якщо на повну точність f64. Діють всі правила текстового імпорту-експорту (див. частину 5) з фіксацією культурно-незалежних принципів. У частині 5 описувалось, у яких випадках десяткове представлення може стати надто дорогим.

Ефективне для машинної взаємодії шістнадцяткове представлення (як 0x1.921fb54442d18p+1 для π) не дозволено. В стандартному JSON немає можливости представити явно Infinity чи NaN; але конкретна реалізація може замість числа з рухомою крапкою експортувати і імпортувати якесь інше значення (наприклад, рядок «Infinity»). Далі питання, чи є схема, яка дозволятиме і валідуватиме такі значення.

За межами вказаних обмежень, головним є, яку точність підтримуватиме конкретна реалізація. Сам по собі JSON не визначає обмежень на точність і діапазон порядку, але специфікації, як IETF RFC 8259, явно вказують, що переважна маса реалізацій буде підтримувати тільки 64-бітний двійковий IEEE754 («double» в C, Java, C#, «float» в Python, «number» в JavaScript).

Фіксація на десятковому транспорті в базовому JSON може стати задорогою для випадку обмежених процесорних ресурсів. Не здивуюсь, якщо десь в embedded світі, якщо вже форсований JSON, такі числа передаються рядками в текстовому шістнадцятковому, чи навіть цілими, які мають те ж побітове представлення; але я не бачив таке в реалі. Але у таких використаннях я більше повірю в бінарні транспорти, як protobuf, чи взагалі кастомні фіксовані структури.

Розширений формат JSON5 дозволяє шістнадцяткові представлення, Inf і NaN. Наскільки я знаю, його частка у всіх використаннях зникаюче мала.

YAML

Схоже на JSON (і є його розширенням в деяких сенсах), але є явні представлення .nan, .inf. Шістнадцяткових представлень немає. Границі точності і порядку віддані реалізаціям.

XML

Загального принципу немає, хоча деякі поширені схеми і їх рушії лімітують представлення, які вони знають (ґенерують код з них для експорту-імпорту і валідації) відомим вже варіантом десяткового текстового представлення. Без них може бути все що завгодно, що вкладається в загальні обмеження ЗСМФ (представлення як текст). (Хоча є CDATA... тут Шахерезада отримала по голові і замовкла.)

protobuf і інші

Один з найрозповсюдженіших СМФ, стараннями Google, який використовує його в своїх транспортах і надав інструменти для всіх мов, які хоч десь в ходу. Бінарний в основі.

Для protobuf маємо так звані «wire type» I64 і I32, в яких вже засобами схеми вказано, що передаються відповідно double і float (в термінах C), бінарно, без обмеження на значення (Infinity, NaN дозволені). На вищому рівні одне повідомлення в protobuf є списком з елементів: пара {теґ, wire type}, за якою слідує зміст. Для данного опису нам достатньо, приклади бінарної передачі значень були представлені. Це як раз найпростіший випадок: стандартно розповсюджені значення передаються 1:1, максимум що треба це подбати про порядок байт (little-endian в protobuf).

Дуже багато СМФ з бінарним транспортом зроблені по такому ж принципу, що дає максимум ефективности для типових випадків останніх 30-40 років. BSON, MessagePack, XDR, Thrift, продовжуйте на свій смак.

ASN.1 + BER, PER, OER

А тут вже цікаво подивитись на підхід, який був розроблений до часів IEEE754 і орієнтований на максимальний степінь взаємодії в світі, де ще були компʼютери на окремих транзисторах і з розміром слова, що не є степенем двійки.

Один з найстарішіх поширених мов опису структур (ASN.1) і форматів передачі у бінарному вигляді (BER). Сам по собі ASN.1 і представлення в BER (і його уточненнях CER, DER) використовується майже на кожному компʼютері, але в обмежених цілях; у пересічного читача це будуть сертифікати сайтів для HTTPS. Проте, в цих сертифікатах не використовуються значення рухомої крапки. Мабуть, друге за розповсюдженістю, де таке уже зустрінеться, це Active Directory від Microsoft або LDAP як споріднена технолоґія. Стандарти на ASN.1 зараз вільно доступні.

Всі представлення числа з рухомою крапкою в ASN.1 бувають або спецзначеннями (як ±Infinity, NaN, —0.0), або представленням в right units view, тобто (M × (b↑E), дe: M — мантисса — ціле зі знаком; b — основа (2, 8, 10, або 16); E — порядок — ціле зі знаком. Вимога бути для мантиси цілим числом дає специфічний, з точки зору звичного, вигляд (частково повторюємо частину 1). Число π, якщо взяте з точністю IEEE float64, має цілу частину 3, але його порядок в представленні ASN.1 буде не 1 (як для left units view, 1⩽M<2), і не 2, як для fraction view (½⩽M<1), а —48: π ≈ 884279719003555 × 2↑-48. Зовнішні представлення вже оформлюють деталі цього базового.

Елементарне дане в BER представляється як послідовність октетів (нагадую — октет це як байт, тільки октет😼). Спочатку іде один чи більше октетів типу. Не вдаватимусь в деталі усяких context-specific типів і теґування; якщо без всього цього, буде один октет зі значенням 9. Далі буде довжина самого значення в октетах; вважаємо, теж один октет. Далі — головний октет, що задає варіант представлення значення. Тут вже ділимо на двійкові і десяткові представлення. Всі октети будуть представлені в цьому описі в шістнадцятковому вигляді (наприклад, AE значить десяткове 174), все довше одного октету безальтернативно представлено в big-endian.

Двійкове представлення розкладається на мантису і порядок; мантиса є ціле число зі знаком, тобто, це відповідає right units view (див. частини 1, 3). Фактичні обмеження на значення є, але вони невикористовно великі (як і всі обмеження в цьому форматі): порядок може мати до 255 октетів зі знаком, що дає сам порядок від −2↑2048 до 2↑2047-1... порядок більше 600 десяткових цифр!😱 Границі значень мантиси ще більші. Практично, знову, питання до схем і реалізацій, жодна реально не дозволяє таких великих значень. Є спеціальні представлення нескінченности зі знаком (було в версії 2007 року), NaN (без уточнень) і −0.0 (обидва додані в версії 2021 року; як на мене, хтось був надто гальмуватий).

Взагалі, повне представлення BER (крім спецзначень) це ((-1)↑S) × N × (2↑F) × (b↑E); S — знак (0 чи 1); N — «чиста» мантиса як ціле без знаку; F — scaling factor (ціле від 0 до 3), повна мантиса M = (-1)↑S × N × 2↑F; b — основа (2, 8 чи 16); E — порядок (ціле зі знаком). Все це, бачимо, було зроблено для того, щоб усидіти на двох стільцях, що розʼїжджаються — зробити максимально незалежно від архітектури, і одночасно мінімізувати затрати на експорт і імпорт для випадку такої ж архітектури на іншому кінці...

Приклади:

оптимальні і безваріантні: (всюди тут початковий 09 — теґ REAL, далі один октет довжини; не більше одного октету на порядок, якщо він присутній)

  • +0.0: 09 00 (нема октетів даних)
  • -0.0: 09 01 43 (спецзначення «мінус ноль»)
  • +1.0: 09 03 80 00 01 (S=0, b=2, F=0, E=0, N=1)
  • -1.0: 09 03 C0 00 01 (S=1, b=2, F=0, E=0, N=1)
  • +12345.0: 09 03 C0 00 30 39 (S=1, b=2, F=0, E=0, N=12345)
  • +49380.0: 09 03 C0 02 30 39 (S=1, b=2, F=0, E=2, N=12345)
  • π (з 64-бітного IEEE): 09 09 80 D0 03 24 3F 6A 88 85 A3 (S=0, b=2, F=0, E=-48, N=0x3243f6a8885a3=884279719003555)
  • +Infinity: 09 01 40
  • -Infinity: 09 01 41
  • NaN (будь-який): 09 01 42

Або спеціально неоптимальний варіант: закодуємо те ж +1.0, але як 256×16↑-2, а 256 як 32×8, і ще розширимо. Буде: 09 83 00 06 AC FF FE 00 00 20. 09 — теґ 9; 83 00 00 06 — довжина 6 октетів, але через мета-довжину (довжина довжини = 3); AC кодує: b=16, F=3, S=0; FF FE значить порядок —2 (при основі 16), але в 2 октетах замість 1; 00 00 20 кодує мантису 32, але ми додали 2 зайвих нульових октети. Сподіваюсь, ніде не помилився. Навіщо таке? А ось захотів по-дивному зробити, бо дозволяє:)

Більш складними формами вирішив не лякати:)

Десяткове представлення чомусь вибрано текстом. Вказані так звані форми NR1...NR3 з ISO6093; якщо пошукати перескази того стандарту, то NR1 це просто ціле зі знаком в ASCII, NR2 — число з фіксованою точкою в ASCII, NR3 — число з явним порядком в ASCII. Тут мене дивує, навіщо було вказувати в першому октеті, яка з цих форм використана, якщо це видно зі значення. Ну, їх лоґіка часто дивує. Головне в плюс, що число легко читається і пишеться; в мінус — неекономність в два рази порівняно з потенційно більш компактним представленням в 4-бітному BCD, яке тут саме напрошується.

Наприклад, 3.14159 буде передане як 09 08 02 33 2E 31 34 31 35 39 (в NR2; після першого октету тут саме «3.14159») або 09 0C 03 2B 33 31 34 31 2E 35 39 45 2D 33 (в NR3; після першого октету значення текст «+3141.59E-3»)... NR1 тут не підходить, бо число не ціле, а вибір між NR2 і NR3 довільний для BER (і фіксований для CER, DER).

Крім деякої ціни конверсії (значно меншої, ніж між двійковим и десятковим), з цим можна нормально працювати — якщо ви взагалі працюєте з ASN.1. ⍨

CER і DER уточнюють, однаково (для рухомої крапки), фіксований вибір найкоротшого представлення (щоб можна було потім порівнювати побайтно без лоґічного декодинґу), вимагаючи: 1) мантиса — 0 чи непарне число; 2) b=8, b=16 приводяться до b=2; 3) F=0; 4) тип і довжина самого значення, представлення M і E мають бути найкоротшими з коректних. Для десяткових забороняються пробіли в NR1...NR3 і фіксуються деталі представлення знаку, мантиси і порядку.

ASN.1 PER (Packed Encoding Rules), який є основним транспортом в протоколах ґрупи 3GPP (і не тільки), повторює для значень рухомої крапки представлення з BER. Пізніше створений OER (Octet Encoding Rules), в залежности від діапазону мантис і порядків, дозволяє IEEE754 float32, float64, або те ж кодування, що в BER (з обмеженнями DER). Я не бачив OER в реалі; пошук каже про деякі нові телекомунікаційні стандарти, а в фінансах — транспорт ISO 20022. Це цікаво в плані, як проґрес поступово зсуває навіть такі старі ґрупи стандартів.

Реалізація експорту і імпорту таких значень вимагає підтримки операцій розібрати число на цілу мантису і порядок, і зібрати назад, маючи на увазі реальну точність (при імпорті може бути округлення). Це зовсім не складно само по собі, але може вимагати методів, які специфічні для кожного рантайма. Саме це тут, як на мене, головна проблема, це вам не два чотири або вісім байтів переслати.

CBOR

CBOR (IETF RFC 8949), який є сучасним (дуже новим, мало хто встиг його застосувати, але поступово таки зростає) власним стандартом IETF для ролі такого базового структурного метаформату, визначає коди представлень (перший октет) 7/25 (0xF9), 7/26 (0xFA), 7/27 (0xFB) відповідно для 16-, 32-, 64-бітного двійкового IEEE754 значення пооктетно в big-endian, рекомендуючи запис найкоротшого з них, яке точно передає цільове значення. Infinity, NaN дозволені і, скоріше за все, будуть представлені в 16-бітному форматі. Все (двійкове), що може бути так представлено, не вимагає додаткових форматів.

Наприклад:

+12345.0: F9 72 07 (F9 значить 7/25, далі 72 07 — представлення у float16), або FA 46 40 E4 00 (float32), або FB 40 C8 1C 80 00 00 00 00 (float64).

Те, що не може бути представлено одним з таких форматів, далі може бути розглянуто для bigfloat представлення, в якому ставиться теґ 5 перед списком з 2 елементів — порядок і мантиса (зі знаком), і тут вже мантиса — ціле число, і right units view (саме як для ASN.1). Це дає необмежену точність мантиси і порядок від −2↑64 до 2↑64-1. Я не знаю застосування, якому б такого не вистачило. І аналоґічно попередньому, для теґа 4 замість 5 отримуємо десяткове представлення (мантиса — ціле зі знаком, порядок — цілий при основі 10).

Наприклад: +123450.5 у двійковому bigfloat: C5 82 20 1A 00 03 C4 75; C5 — теґ 5; 82 — список з 2 елементів; 20 — число −1 (порядок); 1A 00 03 C4 75 — ціле число 246901 (мантиса) в 4-байтовому форматі. Воно ж у десятковому «decimal fraction»: C4 82 20 1A 00 12 D6 49; C4 — теґ 4; 82 — список з 2 елементів; 20 — число −1 (порядок); 1A 00 12 D6 49 — ціле число 1234505 (мантиса) в 4-байтовому форматі.

Якісні конвертери, крім того, можуть підтримувати інші суттєві деталі. В частині 7 розповідалось про показ точности представлення десяткового значення через хвостові нулі. Модуль cbor2 для Python підримує їх:

>>> binascii.hexlify(cbor2.dumps(Decimal("1e6")), " ")
b'c4 82 06 01'
>>> binascii.hexlify(cbor2.dumps(Decimal("1.0e6")), " ")
b'c4 82 05 0a'

Тобто, це #4[6, 1] і #4[5, 10] відповідно, для #tag[exponent, mantissa]: 1.0e6 кодується так саме, як 10e5, і інакше, ніж 1e6.

Здавалося б, цього достатньо? Але дехто дуже... попередливий(?) придумав і додав до списку IANA формати з теґами 264, 265, 268, 269 (специфікації є в посиланнях зі списку IANA), в яких і порядок може бути необмеженим (якщо представити як bignum); точніше, порядок порядку може бути до (2↑67-1)! Ну, зареєстровано. Цікаво було б побачити такий софт, де це дійсно має сенс... складу в кунсткамеру. (І ще в 268, 269 дозволені Infinity і NaN, які і так можна в закодувати базовими форматами без таких розширень. Хмм...) Обійдемось без прикладів, не бачу сенсу показувати щось з порядками в ундецильйони :)

Хоча конкретні байти і відрізняються, сама задача при експорті розібрати число на цілу мантису і порядок, а при імпорті — зібрати назад, не відрізняється за складністю від випадку ASN.1.

Заключне

Звійсно, бувають більш дивні варіанти. В одному прикладі на stackoverflow була фактично передача значення в ASCII як «X.XX» або «XX.X», де X — одна десяткова цифра, і тільки ці два варіанти, а так як завжди 4 байти, автор питання не міг зрозуміти, як це розкодувати. Ну, буває. Але 99+% випадків вкладається в перелічені раніше.

Тут можна було б багато казати про ефективність конкретних представлень і детальніше — про складність роботи з ними в різних мовах. Для 99.9+% розробників головне — пройти між Сциллою негнучкости і Харибдою неефективности. Зупинимось, поки надто далеко не відійшли. (Хоча якщо хтось запитає...)

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось3
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Монументальна робота, дякую

Підписатись на коментарі