Техническа статия

Инженерни функции в Delphi: Преобразуване на бази, комплексна математика

Инженерното семейство в Excel се чете като най-лесния ъгъл от справочника с функции. DEC2BIN превръща число в двоичен низ. HEX2DEC го връща обратно. IMSUM събира две комплексни числа. Всяко изглежда като упражнение по форматиране. Не са. Зад тези имена се крие кодиране на десетбитово допълнение до две (ten-bit two's complement), до което повечето разработчици не са се докосвали от часа по компютърна архитектура, формат за комплексни числа, който живее изцяло вътре в низове, и побитови оператори, които тихо ще препълнят 64-битово цяло число, ако шифтнете, преди да проверите. Един енджин за електронни таблици, който възпроизвежда Excel точно, не може да закръгли (round off) нищо от това

Функциите се разделят на три групи и всяка група крие различен капан. Преобразуването на бази е свързано с отрицателни числа и прагове за всяка база. Комплексната аритметика е свързана с парсване и форматиране на низ. Побитовите операции са свързани с оставането в рамките на границите на Int64. Тази статия разглежда всяка група, както HotXLS я имплементира, с извикванията в работния лист, които реално бихте написали

Преобразуване на бази и десетбитово допълнение до две

Правата посока е частта, която всеки очаква. DEC2BIN(9) дава "1001", а незадължителен втори аргумент допълва отляво резултата (left-pads) до фиксирана ширина. Капанът е отрицателният вход. Excel не изписва знак минус. Той кодира стойността като десетцифрен низ с допълнение до две (two's complement string) в целевата база, поради което DEC2BIN(-5,10) връща "1111111011", вместо нещо със знак. Аргументът за места (places) се игнорира, след като стойността е отрицателна, защото кодирането вече е фиксирано на десет цифри

Десет цифри са фиксиран бюджет и този бюджет задава представимия диапазон (representable range) за всяка база. В двоична система магнитудът, който преминава (flips) в отрицателната половина, е 512, а модулът на превъртане (wrap modulus) е 1024, така че двоичният низ е със знак само когато е дълъг точно десет знака и стойността му е поне 512. Същата идея се мащабира с базата. Осмичната (Octal) система използва праг на половината (half threshold) от 2^29 и пълен модул от 2^30. Шестнадесетичната (Hexadecimal) използва 2^39 и 2^40. Четецът на HotXLS прилага точно това правило: той акумулира цифрите и едва когато низът е широк десет знака и акумулираната стойност седи на или над прага на половината, той изважда пълния модул, за да възстанови стойността със знак. Деветзначен низ винаги е неотрицателен, колкото и голям да е

Енкодерът е огледалният образ. Неотрицателна стойност се преобразува цифра по цифра и по желание се допълва с нули до заявената ширина, като бива отхвърлена, ако препълва положителния таван на базата или ако заявената ширина е твърде тясна, за да я побере. Отрицателна стойност първо се вкарва в диапазон чрез добавяне на пълния модул, което я превръща в стойност, чието базово представяне винаги е десет цифри, и след това цифрите се излъчват с водещи нули (leading zeros), за да запълнят ширината. Единствената споделена проверка на обхвата – симетричните долна и горна граница за база – е това, което поддържа DEC2BIN, DEC2OCT и DEC2HEX съвместими една с друга в техните краища

Остават преобразуванията между бази, тези като HEX2BIN и OCT2HEX, които променят базата, без да преминават през десетична в името на функцията. Имплементацията не носи отделна рутина за всяка подредена двойка. Тя парсва входния низ до десетична стойност със знак, използвайки базата източник, след което форматира тази десетична стойност в целевата база. Десетичната система е пивотът. Една рутина за парсване и една рутина за форматиране, комбинирани, покриват всяка комбинация, и тъй като двете половини споделят една и съща конвенция за десет цифри със знак, отрицателна стойност преживява пътуването със своя знак непокътнат

Комплексните числа са низове, така че работата е парсване

Excel няма комплексен тип данни (complex data type). Комплексната стойност е низът "a+bi" и всяка функция в семейството IM приема тези низове и връща един. COMPLEX изгражда низа от реална и имагинерна част. IMSUM, IMSUB, IMPRODUCT и IMDIV парсват своите аргументи, правят аритметиката върху цифровите части и форматират резултата обратно в низ. Цифровата работа е алгебра за бакалаври. Трудността е изцяло в надеждното превръщане на текста в две числа с плаваща запетая и точно тук вътрешният парсер си изкарва прехраната

Два детайла в този парсер лесно могат да се объркат. Първият е голата имагинерна единица. Низът "i" означава едно по i, не нула и не грешка, така че когато коефициентът пред суфикса е празен или е самотен знак плюс, парсерът трябва да го прочете като стойност 1, а самотен минус като -1. Пропуснете това и IMSUM("i","i") спира да бъде 2i. Вторият е научната нотация (scientific notation), сблъскваща се със знака, който разделя реалната и имагинерната част. Парсерът намира този разделител, като сканира за плюс или минус, но число, записано като "1.5E-3", съдържа минус, който принадлежи на експонентата. Следователно сканирането отказва да третира плюс или минус като разделител, когато знакът непосредствено преди него е e или E. Без този предпазител (guard) реалната част би била разкъсана наполовина при знака на експонентата и парсването би се провалило при напълно валиден вход

Самият суфикс се запазва, вместо да се нормализира. Excel приема както i, така и j, и HotXLS помни кое е използвал входът, така че форматираният резултат да носи същата буква. Форматирането след това прилага конвенционалните съкращения (shorthands): имагинерна част от едно се отпечатва само като суфикса, минус едно като -i, нулева имагинерна част се свива до обикновено реално число (plain real), а нулева реална част изпуска водещото 0+

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Engineering');
    // Negative input: a ten-bit two's complement, places argument ignored.
    Sheet.Cells[1, 1].Value := Sheet.Calculate('=DEC2BIN(-5,10)'); // 1111111011
    // Complex multiply on two "a+bi" strings.
    Sheet.Cells[2, 1].Value := Sheet.Calculate('=IMPRODUCT("3+4i","1+2i")'); // -5+10i
  finally
    Book.Free;
  end;
end;

Трансцендентните комплексни функции, сред които IMSQRT, IMEXP, IMLN и IMPOWER, не работят в правоъгълни координати. Те преобразуват парснатата стойност в полярна форма, прилагат операцията върху модула и аргумента и преобразуват обратно. Квадратен корен разполовява аргумента и взема корен от модула. Степен (power) умножава аргумента и повдига модула. Извършването му по какъвто и да е друг начин би означавало повторно извеждане на всяка идентичност в правоъгълна форма, което е едновременно повече код и по-малко числено стабилно близо до разрезите на клоновете (branch cuts)

Побитови оператори и препълването, което трябва да проверите първо

Excel 2013 добави BITAND, BITOR, BITXOR, BITLSHIFT и BITRSHIFT. Операндите са ограничени: всеки трябва да бъде неотрицателно цяло число, не по-голямо от 2^48 минус 1, и всеки дробен или отрицателен аргумент е числова грешка. Този таван (cap) е достатъчно щедър, за да покрие всеки реалистичен набор от флагове, докато остава добре вътре в точно представимия диапазон (exactly representable range) на double, което има значение, защото Excel предава всеки цифров аргумент като стойност с плаваща запетая

Функциите за отместване (shift functions) носят едното правило за подреждане, което наистина хапе. Лявото отместване (left shift) може да произведе стойност, далеч по-голяма от нейния вход, и ако извършите shl първо и инспектирате резултата след това, вие вече сте препълнили Int64 и тестът е безсмислен. Проверката трябва да дойде преди отместването. HotXLS сравнява операнда с тавана, отместен надясно със сумата на отместване, и само ако операндът се побира, той извършва действителното ляво отместване. Магнитуд на отместване над 53 бита се отхвърля категорично, а отрицателно отместване просто обръща посоката, така че BITLSHIFT с отрицателен брой (count) се държи като дясно отместване (right shift). Принципът се обобщава далеч отвъд тази една функция: когато съществува предпазител за предотвратяване на препълване, той трябва да се изпълнява върху входовете, никога върху резултата, който е трябвало да защити

// Bitwise calls evaluate the same way through Calculate.
Sheet.Cells[3, 1].Value := Sheet.Calculate('=BITAND(13,11)');    // 9
Sheet.Cells[4, 1].Value := Sheet.Calculate('=BITLSHIFT(5,2)');   // 20
Sheet.Cells[5, 1].Value := Sheet.Calculate('=BITRSHIFT(40,3)');  // 5

Бъдещи функции и префиксът на името _xlfn

Побитовите оператори и дълъг списък от други допълнения след 2007 г. си взаимодействат със схема за именуване, която няма нищо общо с това какво те изчисляват и всичко общо с това как Excel ги съхранява. Оригиналният двоичен формат на работен лист (binary worksheet format) присвояваше на всяка вградена функция числов слот (numeric slot) във фиксирана таблица. Функциите, изобретени след като тази таблица беше замразена, нямат слот. За да се запази такава функция във файл и модерен Excel да я разпознае, името се записва с префикс _xlfn., така че BITAND се съхранява като _xlfn.BITAND на диск, въпреки че потребителя въвежда само BITAND

Уловката е, че правилото не е еднообразно (uniform). На някои по-нови функции бяха дадени слотове в таблицата и те се записват голи (bare), докато няколко наследени скрити функции (legacy hidden functions) също се записват без префикс въпреки тяхната възраст. HotXLS поддържа изричен бял списък (whitelist) за това кои имена се нуждаят от префикса, добавя го при запис и го премахва (strips) при четене, така че текстът на формулата, който задавате и четете обратно, винаги е чистото име, насочено към Excel. Вие задавате =BITLSHIFT(5,2), файлът държи _xlfn.BITLSHIFT и стойността се връща като 20 независимо от това. Префиксът е детайл от съхранението, който никога не трябва да изтича (leak) във формулите, с които работите в кода

Сглобяване в работен лист

Публичната повърхност (public surface) за всичко това е малка. Създайте TXLSXWorkbook, добавете работен лист и или напишете формула в клетка чрез Cells[Row, Col].Formula и преизчислете (recalculate), или оценете израз директно с метода Calculate на работния лист, който компилира формулата спрямо този лист и връща Variant. Примерите по-горе използват Calculate, защото той показва резултата от едно инженерно извикване без заобикалящото състояние на листа, но същите функции се оценяват идентично вътре в реални формули в клетки (cell formulas), когато работната книга се преизчислява

Кодиранията са частта, която трябва да имате предвид, а не местата на извикване (call sites). Двоичният низ е със знак само при десет цифри и само след прага на половината за своята база. Комплексното число е текст, празният имагинерен коефициент е едно и парсерът прекрачва e на експонентата. Лявото отместване се проверява, преди да отмести. Справете се с тези четири факта и инженерното семейство спира да бъде източник на изненади от типа "сбъркано с един знак" (off-by-a-sign surprises)

Ако свързвате (wiring) вашата собствена домейн математика към същия енджин, механиката на регистриране на манипулатор (handler) и връщане на стойности е обхваната в нашата статия за разширяване на енджина за формули с персонализирани функции, а когато тези формули трябва да достигат през различни листове по име, а не по адрес на клетка, ръководството за дефинирани имена и формули между листове (cross-sheet formulas) показва как препратките се разрешават. Инженерните функции, описани тук, се доставят като част от компонента за електронни таблици HotXLS за Delphi и C++Builder, заедно с API-тата за четене, формули и форматиране, обхванати на други места в този блог