Журнал LinuxFormat - перейти на главную

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

Рис. 1 Рис. 1. Пример использования встроенной функции CONCATENATE.

Рис. 2a Рис. 2b Рис. 2. Реализация перегрузки: традиционная — a, и средствами кода — b.

Это реализация двоичной функции 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) для функции, пользователь будет ожидать обработки всех переданных элементов. Он просто не будет отсчитывать, сколько параметров передал, а если и будет, то может банально сбиться со счёта. В итоге последний элемент, или даже несколько элементов, не будут добавлены в результат, а пользователь может и не обратить внимания на это. А что если это будет как раз та самая запятая в «Казнить нельзя помиловать»? Так что лучше уж дать пользователю знать, что не все элементы обработаны, сгенерировав ошибку.

Псевдонимы

Рис. 3a Рис. 3b Рис. 3. Перегрузка — a, и алиасы — b.

Механизм перегрузки позволяет иметь несколько разных функций под одним именем; псевдонимы (алиасы), напротив, позволяют обращаться к одной и той же функции по разным именам. В некотором смысле, алиасы – приём, обратный перегрузке (рис. 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

Рис. 4. Применение COMBINE и СОБРАТЬ.

Персональные инструменты
купить
подписаться
Яндекс.Метрика