- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF133:OOo Calc
Материал из Linuxformat.
- Пользовательские функции. Добавьте электронным таблицам недостающий функционал
Содержание |
OOo Calc: Без исключений
OOo Calc |
---|
|
- В прошлый раз мы рассмотрели базовые приёмы создания функций. Сегодня Александр Маджугин поведает, как сделать ваши функции более удобными в использовании.
Мы расскажем о том, как не поставить пользователя в затруднительное положение, выбрасывая исключения Basic в ответ на малейшие его ошибки в использовании функции (при этом предполагается, что код самой функции ошибок не содержит), и о том, как сделать функции более универсальными, используя имитацию механизма перегрузки.
Для тех, кто забыл, напомним, что перегрузка – это возможность создавать различные функции, с разным количеством параметров или с различными их типами, но вызываемые по одному имени. Это может быть очень удобно, если такие функции выполняют однотипные операции.
Работа над ошибками
В первую очередь следует рассмотреть обход ошибок в функциях: даже если ее единственным пользователем будете вы сами, прерывание расчета сообщением об исключении и остановка Basic – это все равно неприятно. Было бы куда удобнее получать строковое значение ошибки непосредственно в ячейку функции как значение. Если же вы считаете, что с вашей функцией будут работать другие пользователи, особенно незнакомые с макросами и Basic, то просто обязаны исключить ситуацию, когда ошибка в передаче параметров приводит к аварийному завершению расчета функции.
Работу по перехвату и корректной обработке ошибок следует начинать с того, чтобы добавить функции возможность возвращать строковое значение (если изначально оно не было строковым или вариантным). Давайте посмотрим на код следующей очень простой функции:
Public Function NDS (ByVal cInSumm As Double, ByVal cStavka As Double) As Double NDS = (cInSumm/(100 + cStavka)) * cStavka End Function
Это вариант кода из LXF132, рассчитывающего сумму НДС, включенную в стоимость, с тем отличием, что в данной функции ставка налога cStavka должна быть передана в качестве параметра при её вызове. Как и ранее, функция возвращает Double. Для возврата кода ошибки нам требуется String, однако применить его напрямую мы не можем, так как Double тоже необходим.
В таких случаях следует определить тип возвращаемого значения функции как Variant и использовать дополнительную типизацию результата через промежуточную переменную. Наиболее простой способ обработки ошибки при этом будет выглядеть так:
Public Function NDS (ByVal cInSumm As Variant, ByVal cStavka As Variant) As Variant On Error Goto ErrNDS Dim dResult As Double Dim sError As String dResult = cInSumm/(100 + cStavka) dResult = dResult * cStavka NDS = dResult Exit Function ErrNDS: sError = “#ERROR!” NDS = sError End Function
Теперь в результате любой ошибки мы будем получать сообщение «#ERROR!» в ячейке с функцией, что уже гораздо лучше, чем прерывание работы Basic. Но если вы хотите, чтобы ваша функция давала понять, где произошла ошибка, придётся потрудится ещё, чтобы обрабатывать каждую из них по-своему.
Здесь есть два пути. Первый – это перехват ошибки и последующий анализ того, что произошло, с выводом соответствующего кода сообщения. Этот путь наиболее простой и… неправильный. Неправильный не с точки зрения работы кода, а с позиций идеологии. Строить работу программы на перехвате ошибки – просто дурной тон. Именно поэтому мы пойдём по второму пути: постараемся не допустить ошибки, ну или хотя бы большинства из них, проверяя параметры на допустимость перед вычислением.
Сначала ограничим значение параметра cStavka – так, чтобы не допустить деления на ноль. Для этого в самом начале процедуры добавим следующие строчки:
If cStavka = -100 Then NDS = “#DIV/0!” Exit Function End If
Мы могли бы здесь ограничить область определения параметра cStavka положительными числами до 100, так как только такая ставка налога имеет практический смысл, но мы не знаем, для чего будет применять нашу функцию конечный пользователь, поэтому постараемся минимально ограничить его свободу.
Можно заметить, что мнемокод нашей ошибки отличается от стандартного мнемокода ошибки деления на 0 – это было сделано намеренно, чтобы пользователь мог отличить ошибки, выводимые Calc, от ошибок, генерируемых нашей функцией, которые являются просто текстовыми строками и не могут обрабатываться функциями ошибок Calc.
Маловато будет!
Следующей нашей задачей будет обработка ситуации нехватки переданных параметров. Сейчас, если мы передадим в функцию только один параметр (=NDS(A1)) то столкнёмся с ошибкой «Аргумент является обязательным.» в отношении параметра cStavka.
Чтобы корректно обходить такие ошибки, нам потребуется сделать все аргументы функции опциональными:
Public Function NDS (ByVal Optional cInSumm As Variant, ByVal Optional cStavka As Variant) As Variant
Это позволит избежать ошибки во многих случаях, но результат функции будет слабо предсказуем. Например, в данном случае вызов функции в Calc с одним аргументом приведёт к расчёту налога со ставкой 22 %. Такое поведение функции нам ни к чему, поэтому перехватим все непереданные параметры функции до начала вычисления. В этом нам поможет встроенная функция Basic IsMissing(): она принимает имя входящего аргумента и возвращает True, если аргумент не был передан, и False – в противном случае. И раз мы сделали аргументы опциональными, присвоим им значения по умолчанию. Для обработки параметра cStavka, код, который следует добавить в начале функции, может выглядеть так:
If IsMissing (cStavka) then cStavka = 18 End If
Теперь значение cStavka по умолчанию будет равняться 18 %. А какое выбрать значение по умолчанию для аргумента cInSumm? Можно, конечно, сделать его равным 0, но польза от этого весьма сомнительна; да и вообще, если уж потерян и этот аргумент, функция теряет смысл. Лучше уж просто возвратить ошибку:
If IsMissing (cInSumm) then NDS = “#NOTARG!” Exit Function End If
Что же, осталась всего одна ситуация, которой хотелось бы избежать: несовпадение типов переданных нам параметров с типами, требуемыми для вычисления. Очевидно, все аргументы данной функции должны быть числами; проверим это функцией IsNumeric(), возвращающей True, только если аргумент – число:
If IsMissing (cStavka) then cStavka = 18 ElseIf Not(IsNumeric(cStavka)) Then NDS = “#INVT!” Exit Function End If
По аналогии проверим и тип cInSumm. Всё? Нет! Серьёзная проблема данной функции – то, что в качестве второго аргумента она принимает процентное значение; вобщем-то, потому она и была выбрана для данного примера. Традиционно в электронных таблицах проценты представляются сразу в виде сотых долей. То есть 18 % хотя и отображаются в ячейки с процентным форматом как «18 %», но в числовом виде представлены, как правило, значением 0,18. Так что с точки зрения Calc проценты передаются в нашу функцию не совсем правильно. С другой стороны, на практике многие неискушённые в работе с электронными таблицами пользователи стремятся применять для процентов числа, представляющие их количественное выражение, то есть записывают 18 % как целое число 18. И с точки зрения этих пользователей наша функция работает правильно. Остаётся выбрать, на чьей стороне мы находимся, и либо немного изменить нашу функцию, либо оставить всё как есть.
Я предлагаю выбрать третий путь – постараться угодить и тем, и тем. Строго говоря, такое решение как минимум спорное, но зато очень показательное в отношении того, как можно обыгрывать входящие аргументы. Итак, добавим дополнительную проверку аргумента cStavka: если его значение находится в диапазоне от 0 до 1, будем рассматривать его как сотые доли, а если оно выходит за этот диапазон – как целые проценты:
If cStavka>-1 AND cStavka<1 then cStavka = cStavka*100 End If
Такое поведение функции можно описать в справке к ней, и хотя оно может приводить к ошибкам у пользователей, применяющих её не по назначению и проигнорировавших справку, у тех, кто будет использовать её для расчёта НДС, включённого в стоимость, функция будет работать правильно независимо от уровня подготовки и познаний в электронных таблицах.
Итак, мы рассмотрели не только перехват ошибки вычислений в функции, но и обработку ошибки пользователя в его работе с электронными таблицами. Полный листинг получившейся функции вы найдете на диске (файл Listing1.txt).
Перегрузка
Хотяв StarBasic нет стандартных механизмов для перегрузки функций, процедур или операторов, несомненно, используя проверку переданных аргументов, мы можем создавать фактически перегруженные функции. Рассмотрим два простых примера того, как этого можно достигнуть практически в любой реализации Basic.
Первый пример демонстрирует классическую перегрузку функции – мы просто выбираем одну из двух заранее созданных функций на основе предварительной проверки параметров:
Public Function FXOR (ByVal Optional vFirst As Variant, ByVal Optional vSecond As Variant) As Variant If IsMissing (vFirst) Or IsMissing (vSecond) Then ' проверяем переданы ли параметры fXOR = “#NOTARG!” ‘ аргументов слишком мало Exit Function End If ‘ проверяем тип и выбираем нужную функцию If IsNumeric (vFirst) And IsNumeric (vSecond) Then ' Если оба параметра - числа... fXOR = NXOR (vFirst, vSecond) ' ...вызываем функцию для чисел Exit Function End If If VarType(vFirst)=8 And VarType(vSecond)=8 Then ' Если оба параметра - строки... fXOR = SXOR (vFirst, vSecond) ' ...вызываем функцию для строк Exit Function End If fXOR = “#NCT!” ' Несовместимые типы! End Function
Private Function NXOR (ByVal Optional nFirst As Variant, ByVal Optional nSecond As Variant) As Variant NXOR = nFirst XOR nSecond End Function
Private Function SXOR (ByVal Optional sFirst As Variant, ByVal Optional sSecond As Variant) As Variant Dim l As Long Dim lLF As Long Dim lLS As Long Dim sResult As String lLF = Len(sFirst) lLS = Len(sSecond) For l = 1 To lLF sResult = sResult & Chr(ASC(Mid(sFirst,l,1)) XOR ASC(Mid(sSecond,(l mod lLS)+1,1))) Next
SXOR = sResult End Function
Это реализация двоичной функции XOR, которую можно применять к числам (и тогда она возвращает число – результат побитовой операции над аргументами) или к строкам (и в этом случае возвращается строка – результат все той же побитовой операции). Причём во втором случае, если вторая строка короче первой строки, то её символы повторяются требуемое число раз. Здесь первую строку можно считать открытым текстом, вторую – ключом, а результат – шифротекстом. Отсюда видно, что функция FXOR не коммутативна для строк, так как в общем случае FXOR(строка1;строка2)<>FXOR(строка2;строка1), а для чисел она коммутативна. Таким образом, мы получаем две совершенно разные функции под одним именем, и фактически имеем перегруженную функцию.
Здесь же налицо и основная проблема перегрузки – трудность выбора нужной функции в отдельных ситуациях. Если оба параметра – строки или числа, мы знаем, что делать. А если один из параметров – число, а другой – строка? Вышеприведённая функция вернёт ошибку «#NCT!», сообщающую о несовместимости типов. Но кто сказал, что строку нельзя зашифровать числовым паролем? Видимо, ситуация, где первый аргумент – строка, а второй – число, всё же достойна обработки. А если первый аргумент – число, а второй – строка?
В этой функции только два аргумента, и оба обязательны, поэтому возможных комбинаций всего четыре. А если таких аргументов 31, да ещё и опциональных? Тогда вопрос о перегрузке придётся решать для каждого аргумента отдельно – просто выбрать одну из двух возможных функций будет уже недостаточно. Именно так обстоят дела в следующем примере.
Объедини это
В Calc есть отличная функция CONCATENATE, позволяющая объединить текст из нескольких ячеек. Однако она не лишена недостатков: во-первых, она не умеет обрабатывать диапазон; во-вторых, если требуется объединить ячейки через разделитель, его нужно указывать многократно, как очередной аргумент функции.
Результат работы функции в простом примере показан на рис. 1. Как видно, зрелище не самое привлекательное. Чтобы сделать надпись более-менее читаемой, придётся использовать нечто вроде CONCATENATE(Ai;”“;Bi;”“;Ci;”“;Di;”“;Ei;”“;F$2;”“;TEXT(Fi;“DD.MM.YY”);” “;Gi) — это, как минимум, не верх удобства.
Всё могло бы измениться, если бы CONCATENATE умела работать с диапазонами и поддерживала бы разделитель. Такая функция требовала бы перегрузки, причём по каждому параметру, так как для объединения одного текстового элемента и массива требуются разные процедуры в силу различных типов, а предугадать их порядок и комбинацию невозможно. Именно такая функция – COMBINE – представлена на следующем листинге:
Public Function COMBINE (ByVal Optional sDiv As Variant, _ ByVal Optional s0 As Variant, _ ByVal Optional s1 As Variant, _ ... ByVal Optional s29 As Variant, _ ByVal Optional ManyArg As Variant) As Variant
Dim Result As String
If IsMissing (sDiv) Then ' функция вызвана без аргументов COMBINE = “#NOTARG!” Exit Function End If
If IsMissing (s0) Then ‘ проверяем передан ли параметр COMBINE = “#NOTARG!” ‘ аргументов слишком мало Exit Function Else If IsArray (s0) Then ‘ если s1 диапазон - вызываем соответствующую процедуру... Result = Result & CONCATENATE_ARRAY(s0, sDiv) ' и добавим к Result её результат Result = Right(Result,Len(Result)-Len(sDiv)) ‘ убираем начальный разделитель Else ‘ в противном случае... Result = s0 ‘ просто добавим s0 к результату End If End If ' s1 If IsMissing (s1) Then ‘ проверяем передан ли параметр COMBINE = Result Exit Function Else If IsArray (s1) Then ‘ если s1 диапазон - вызываем соответствующую процедуру... Result = Result & CONCATENATE_ARRAY(s1, sDiv) ' и добавим к Result её результат Else ‘ в противном случае... Result = Result & sDiv & s1 ‘ просто добавим s1 к результату End If End If ... If IsMissing (ManyArg) Then ' проверяем передан ли параметр COMBINE = Result Else COMBINE = “#TMA!” End If End Function
Function CONCATENATE_ARRAY(ByVal aArray As Variant, ByVal sDiv As String) Dim s1 As String Dim s2 As String For Each s2 In aArray s1 = s1 & sDiv & CStr(s2) Next CONCATENATE_ARRAY = s1 End Function
Она имеет 32 аргумента, причем со 2‑го по 31‑й они идентичны – и для сокращения объёма листинга заменены на троеточие (…). Все они эквивалентны соответствующим элементам s1 и отличаются только номерами.
Обратите внимание, что это функция требует также подфункции CONCATENATE_ARRAY, которая тоже присутствует в приведённом листинге.
Здесь перегрузка выполняется для каждого отдельного аргумента, с помощью конструкции
If IsArray (s2) Then ‘ если s2 диапазон - вызываем соответствующую процедуру... Result = Result & CONCATENATE_ARRAY(s2, sDiv) ' и добавим к Result её результат Else ‘ в противном случае... Result = Result & sDiv & s2 ‘ просто добавим s2 к результату End If
в которой определяется, является ли параметр sX массивом, и если да, то вызывается соответствующая процедура CONCATENATE_ARRAY.
Без сомнения, это не совсем то, что принято называть перегрузкой в языках, поддерживающих данный механизм, однако по смыслу это то же самое, что хорошо демонстрирует рис. 2.
Теперь вместо CONCATENATE(Ai;” “;Bi;” “;Ci;” “;Di;” “;Ei;” “;F$2;”“;TEXT(Fi;“DD.MM.YY”);” “;Gi), мы можем использовать COMBINE(““;Ai:Ei;F$2;TEXT(Fi;“DD.MM.YY”);Gi), с тем же результатом.
Хочется также обратить внимание читателей на аргумент ManyArg, который, в принципе, не нужен для работы функции: его единственная цель – это... генерировать ошибку, если он передан. Эта мера необходима, чтобы ограничить количество объединяемых аргументов в функции тридцатью. «Зачем – ведь можно бы ло бы просто игнорировать лишние аргументы?» – может сказать кто-то. Можно – только это одна из очень распространённых и грубых ошибок при создании функций со многими однотипными повторяющимися аргументами.
Дело в том, что, даже зная об ограничении в N аргументов (здесь – 30) для функции, пользователь будет ожидать обработки всех переданных элементов. Он просто не будет отсчитывать, сколько параметров передал, а если и будет, то может банально сбиться со счёта. В итоге последний элемент, или даже несколько элементов, не будут добавлены в результат, а пользователь может и не обратить внимания на это. А что если это будет как раз та самая запятая в «Казнить нельзя помиловать»? Так что лучше уж дать пользователю знать, что не все элементы обработаны, сгенерировав ошибку.
Псевдонимы
Механизм перегрузки позволяет иметь несколько разных функций под одним именем; псевдонимы (алиасы), напротив, позволяют обращаться к одной и той же функции по разным именам. В некотором смысле, алиасы – приём, обратный перегрузке (рис. 3).
Обычно это не нужно, но обычно – не значит всегда. Например, вам хочется называть локализованную функцию COMBINE именем СОБРАТЬ, но при этом необходимо поддерживать и исходное имя, для совместимости. Можно, конечно, просто повторить функцию под новым именем – СОБРАТЬ, но если она достаточно объёмна, то это не всегда удобно, особенно если помнить про ограничение на длину модуля Basic в 65536 байт. Поэтому проще создать функцию-посредника, которая просто будет вызывать оригинал:
Public Function [СОБРАТЬ] (ByVal Optional sDiv As Variant, _ ByVal Optional s0 As Variant, _ ByVal Optional s1 As Variant, _ ... ByVal Optional s29 As Variant, _ ByVal Optional ManyArg As Variant) As Variant [СОБРАТЬ] = COMBINE (sDiv, s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, _ s11, s12, s13, s14, s15, s16, s17, s18, s19, s20, _ s21, s22, s23, s24, s25, s26, s27, s28, s29, ManyArg) End Function
То есть, в данном случае, функция СОБРАТЬ просто принимает аргументы и, даже не проверяя их, вызывает функцию COMBINE, а результат COMBINE возвращает как свой собственный. На рис. 4 видна работа функций СОБРАТЬ и COMBINE (ячейки A9 и A10) – кратко, понятно и удобно.
Обратите внимание на квадратные скобки, обрамляющие имя функции – это эскейп-имя [escape name]. Заключение имени процедуры, функции или переменной в квадратные скобки позволяет использовать в этих именах запрещённые символы – в данном случае, кириллицу. Это довольно распространённая практика; однако официально в StarBasic эскейп-имена отсутствуют, и документация о них молчит, так как поддержка их пока неполна. Обычно эскейп-имена позволяют использовать не только запрещённые символы, но и имена, совпадающие с ключевыми словами языка – например, Sub. Но в StarBasic это невозможно: использование имени [SUB] для функции, процедуры или переменной может привести к «падению» пакета. В общем, будьте осторожны.
Рис. 4. Применение COMBINE и СОБРАТЬ.