<?xml version="1.0" encoding="UTF-8"?>

<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
	<channel>
		<title><![CDATA[AXForum - Блоги - Gustav'ово бложище, или Записки DAX-дилетанта-III. Автор Gustav]]></title>
		<link>//axforum.info/forums/blog.php?u=5597</link>
		<description>Microsoft Dynamics: Axapta, CRM, Navision. Форум, Вопросы и помощь специалистов.</description>
		<language>ru</language>
		<lastBuildDate>Fri, 24 Apr 2026 17:35:54 GMT</lastBuildDate>
		<generator>vBulletin</generator>
		<ttl>15</ttl>
		<image>
			<url>http://m.axforum.info//img.axforum.info/misc/rss.jpg</url>
			<title><![CDATA[AXForum - Блоги - Gustav'ово бложище, или Записки DAX-дилетанта-III. Автор Gustav]]></title>
			<link>//axforum.info/forums/blog.php?u=5597</link>
		</image>
		<item>
			<title>Сумма прописью (RU): Axapta, VBA и Excel (в одной ячейке!)</title>
			<link>//axforum.info/forums/blog.php?b=112</link>
			<pubDate>Tue, 13 Mar 2012 11:30:38 GMT</pubDate>
			<description><![CDATA[Это сообщение готовилось к публикации почти два года тому назад. Тогда мне потребовалась сумма прописью в своей разработке. Прошерстив форум, нашёл ссылки на класс RNumDateInWordConverter, но что-то с первого раза с этим классом не получилось. Потом, правда, срослось, но "осадок остался" и появилось горячее желание прикоснуться к теме лично, тем более, что в голове сложился своеобразный алгоритм решения (как мне кажется... или это только мне кажется его своеобразность? :)). Этим алгоритмом мне тогда и хотелось поделиться, но текучка отложила этот радостный момент на неопределенный срок.

Предлагаемый алгоритм основывается на переводе исходного числа (его целой части) в последовательность триад и в обработке каждой триады как числа от 0 до 999. При этом к соответствующей разрядности типа "тысяч", "миллионов", "миллиардов" относимся так же, как к любому считаемому существительному, например, к "рублям" или иным единицам (метрам, штукам и т.п.). Это позволяет вынести алгоритм обработки триады в отдельную, достаточно компактную функцию, снаружи которой процесс сводится к нескольким ее вызовам с соответствующей считаемой единицей в качестве параметра.

С деталями алгоритма можно ознакомиться по приведенным ниже текстам метода sumInWords_RU (для Аксапты) и одноименной же функции (для VBA).

// KKu, 23.04.2010 --> РУССКАЯ СУММА ПРОПИСЬЮ

//  _sourceReal - вещественное число для прописи (минус и дробная часть игнорируются при обработке)
//  _unit1,_unit2,_unit5 - формы считаемого существительного соответственно для 1 единицы, 2 ед-ц и 5 ед-ц
//  _gender - код рода считаемого существительного = 1-мужской, 2-женский, 3-средний
//  _capital - в возвращаемой строке делать большими буквы:
//      0-всё маленькими, 1-только самую первую букву всей строки,2-первая буква каждой триады

static str sumInWords_RU( real  _sourceReal,
                          str   _unit1   = 'рубль',  // (один) рубль
                          str   _unit2   = 'рубля',  // (два ) рубля
                          str   _unit5   = 'рублей', // (пять) рублей
                          int   _gender  = 1,        // 1 - мужской (рубль)
                          int   _capital = 1 )       // 1 - только первая всей строки
{
    str         strSource = strFmt('00%1', num2str( trunc((abs(_sourceReal))),1,0,1,0 ));
    int         cntTriad  = trunc(strLen(strSource)/3);
    str         сurrTriad, morePwr12, fRet;
    int         i;

    str triadWords(str triad, str unit1, str unit2, str unit5, int gender)
    {
        str ret  = conPeek(['','сто ','двести ','триста ','четыреста ',
                    'пятьсот ','шестьсот ','семьсот ','восемьсот ','девятьсот '],
                    str2int(subStr(triad,1,1))+1 );
        str currUnit = unit5;
        ;
        if (strSource=='000')
        {
            ret = 'ноль ';
        }
        else if (subStr(triad,2,1)=='1')
        {
            ret += conPeek(['десять','одиннадцать','двенадцать','тринадцать','четырнадцать',
                    'пятнадцать','шестнадцать','семнадцать','восемнадцать','девятнадцать'],
                    str2int(subStr(triad,2,2))-9 ) + ' ';
        }
        else
        {
            ret += conPeek(['','','двадцать ','тридцать ','сорок ',
                    'пятьдесят ','шестьдесят ','семьдесят ','восемьдесят ','девяносто '],
                    str2int(subStr(triad,2,1))+1 );

            ret += conPeek(['',conPeek(['один ','одна ','одно '],gender),
                    conPeek(['два ' ,'две ' ,'два ' ],gender),
                    'три ','четыре ','пять ','шесть ','семь ','восемь ','девять '],
                    str2int(subStr(triad,3,1))+1 );

            currUnit = conPeek([unit5, unit1, unit2,unit2,unit2, unit5,unit5,unit5,unit5,unit5],
                        str2int(subStr(triad,3,1))+1 );
        }
        return ret ? strFmt('%1%2 ', str2Capital(ret), currUnit) : (cntTriad-i+1)==1 ? currUnit : '';
    }
    ;

    strSource = subStr(strSource, strLen(strSource)-cntTriad*3+1, cntTriad*3);
    for (i=1; i<=cntTriad; i++)
    {
        сurrTriad = subStr(strSource, (i-1)*3+1, 3);
        switch (cntTriad-i+1)
        {
            case 1: fRet += triadWords(сurrTriad,_unit1    ,_unit2     ,_unit5,_gender); break; // 10^0
            case 2: fRet += triadWords(сurrTriad,'тысяча'  ,'тысячи'   ,'тысяч'     ,2); break; // 10^3
            case 3: fRet += triadWords(сurrTriad,'миллион' ,'миллиона' ,'миллионов' ,1); break; // 10^6
            case 4: fRet += triadWords(сurrTriad,'миллиард','миллиарда','миллиардов',1); break; // 10^9
            case 5: fRet += triadWords(сurrTriad,'триллион','триллиона','триллионов',1); break; // 10^12
            default:
                    morePwr12 = strFmt('10^%1',(cntTriad-i)*3); // 10^15 и т.д.
                    fRet += triadWords(сurrTriad, morePwr12, morePwr12 , morePwr12  ,1);
        }
    }
    return strRTrim(conPeek([strLwr(fRet), str2Capital(strLwr(fRet)), fRet], _capital+1));
}
VBA:

''//РУССКАЯ СУММА ПРОПИСЬЮ - функция sumInWords_RU
Option Explicit

Dim strSource   As String
Dim cntTriad    As Integer
Dim i           As Integer

Private Function triadWords(ByVal triad As String, _
                            ByVal unit1 As String, _
                            ByVal unit2 As String, _
                            ByVal unit5 As String, _
                            ByVal gender As Integer) As String
                            
    Dim ret         As String
    Dim currUnit    As String
    
    ret = Choose(CInt(Left(triad, 1)) + 1, "", "сто ", "двести ", "триста ", "четыреста ", _
                "пятьсот ", "шестьсот ", "семьсот ", "восемьсот ", "девятьсот ")
    currUnit = unit5
    
    If strSource = "000" Then
        ret = "ноль "
        
    ElseIf Mid(triad, 2, 1) = "1" Then
        ret = ret & Choose(CInt(Right(triad, 2)) - 9, "десять", "одиннадцать", _
                    "двенадцать", "тринадцать", "четырнадцать", "пятнадцать", _
                    "шестнадцать", "семнадцать", "восемнадцать", "девятнадцать") & " "
    Else
        ret = ret & Choose(CInt(Mid(triad, 2, 1)) + 1, "", "", _
                    "двадцать ", "тридцать ", "сорок ", "пятьдесят ", _
                    "шестьдесят ", "семьдесят ", "восемьдесят ", "девяносто ")
        ret = ret & Choose(CInt(Right(triad, 1)) + 1, "", _
                    Choose(gender, "один", "одна", "одно") & " ", _
                    Choose(gender, "два", "две", "два") & " ", _
                    "три ", "четыре ", "пять ", "шесть ", "семь ", "восемь ", "девять ")
        currUnit = Choose(CInt(Right(triad, 1)) + 1, _
                    unit5, unit1, unit2, unit2, unit2, _
                    unit5, unit5, unit5, unit5, unit5)
    End If
    
    triadWords = IIf(ret <> "", UCase(Left(ret, 1)) & Mid(ret, 2) & currUnit & " ", _
                                IIf((cntTriad - i + 1) = 1, currUnit, ""))
End Function

''//РУССКАЯ СУММА ПРОПИСЬЮ
''//  sourceReal - вещественное число для прописи (минус и дробная часть игнорируются при обработке)
''//  unit1, unit2, unit5 - формы считаемого существительного соответственно для 1 единицы, 2 ед-ц и 5 ед-ц
''//  gender - код рода считаемого существительного = 1-мужской, 2-женский, 3-средний
''//  capital - в возвращаемой строке делать большими буквы:
''//     0-всё маленькими, 1-только самую первую букву всей строки,2-первая буква каждой триады
Public Function sumInWords_RU(ByVal sourceReal As Double, _
                              Optional ByVal unit1 As String = "рубль", _
                              Optional ByVal unit2 As String = "рубля", _
                              Optional ByVal unit5 As String = "рублей", _
                              Optional ByVal gender As Integer = 1, _
                              Optional ByVal capital As Integer = 1) As String

    Dim сurTrd      As String
    Dim morePwr12   As String
    Dim fRet        As String
   
    strSource = "00" & Format(Int(Abs(sourceReal)), "0")
    cntTriad = Int(Len(strSource) / 3)
    strSource = Right(strSource, cntTriad * 3)
    
    For i = 1 To cntTriad
        сurTrd = Mid(strSource, (i - 1) * 3 + 1, 3)
        Select Case cntTriad - i + 1
            Case 1: fRet = fRet & triadWords(сurTrd, unit1, unit2, unit5, gender)
            Case 2: fRet = fRet & triadWords(сurTrd, "тысяча", "тысячи", "тысяч", 2)
            Case 3: fRet = fRet & triadWords(сurTrd, "миллион", "миллиона", "миллионов", 1)
            Case 4: fRet = fRet & triadWords(сurTrd, "миллиард", "миллиарда", "миллиардов", 1)
            Case 5: fRet = fRet & triadWords(сurTrd, "триллион", "триллиона", "триллионов", 1)
            Case Else
                morePwr12 = "10^" & CStr(cntTriad - i) * 3
                fRet = fRet & triadWords(сurTrd, morePwr12, morePwr12, morePwr12, 1)
        End Select
    Next i
    
    sumInWords_RU = RTrim(Choose(capital + 1, LCase(fRet), Left(fRet, 1) & LCase(Mid(fRet, 2)), fRet))
End Function
Но что же заставило меня наконец перевести это сообщение из статуса "черновик" в открытый доступ? А вот что - на основе ранее разработанных вышеприведенных функций сочинилась формула для Excel, помещающаяся в одной ячейке! Т.е. в ячейку А1 вводим число, в ячейку B1 - формулу и в ней же читаем сумму прописью. Никаких макросов, весь текст собирается в одной ячейке с использованием стандартных функций рабочего листа (у кого-нибудь есть образец заявки в книгу рекордов Гиннесса? :)) 

По терминологии маэстро Дж.Уокенбаха (http://spreadsheetpage.com/) эта формула - мегаформула (не в смысле, что такая крутая, а потому что без промежуточных результатов). С количеством символов около 6 тысяч - поэтому может использоваться только в версии Excel, начиная с 2007, когда допустимая длина формулы увеличилась с 1024 до 8192 символов. Для более ранних версий, однако, возможен "расчет" суммы прописью с задействованием нескольких соседних ячеек (и существенным сокращением общего кол-ва формульных символов за счет использования формул массива). Но об этом позже, а сейчас - вот эта базовая формулища:

МЕГАФОРМУЛА ДЛЯ ОДНОЙ ЯЧЕЙКИ EXCEL:

=ПОДСТАВИТЬ(ПОДСТАВИТЬ(ПРОПНАЧ(
ЕСЛИ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));15);3)
+0=0;""
;
ВЫБОР(ЛЕВСИМВ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));15);3)
)+1
;"";"стоs";"двестиs";"тристаs";"четырестаs";
"пятьсотs";"шестьсотs";"семьсотs";"восемьсотs";"девятьсотs")
&
ЕСЛИ(ПСТР(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));15);3)
;2;1)+0=1;
ВЫБОР(ПРАВСИМВ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));15);3)
;2)-9
;"десятьs";"одиннадцатьs";"двенадцатьs";"тринадцатьs";"четырнадцатьs";
"пятнадцатьs";"шестнадцатьs";"семнадцатьs";"восемнадцатьs";"девятнадцатьs")
&"триллионов "
;
ВЫБОР(ПСТР(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));15);3)
;2;1)+1
;"";"";"двадцатьs";"тридцатьs";"сорокs";
"пятьдесятs";"шестьдесятs";"семьдесятs";"восемьдесятs";"девяностоs")
&
ВЫБОР(ПРАВСИМВ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));15);3)
)+1
;"триллионов ";"одинsтриллион ";"дваsтриллиона ";"триsтриллиона ";"четыреsтриллиона ";
"пятьsтриллионов ";"шестьsтриллионов ";"семьsтриллионов ";"восемьsтриллионов ";"девятьsтриллионов ")
))
&
ЕСЛИ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));12);3)
+0=0;""
;
ВЫБОР(ЛЕВСИМВ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));12);3)
)+1
;"";"стоs";"двестиs";"тристаs";"четырестаs";
"пятьсотs";"шестьсотs";"семьсотs";"восемьсотs";"девятьсотs")
&
ЕСЛИ(ПСТР(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));12);3)
;2;1)+0=1;
ВЫБОР(ПРАВСИМВ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));12);3)
;2)-9
;"десятьs";"одиннадцатьs";"двенадцатьs";"тринадцатьs";"четырнадцатьs";
"пятнадцатьs";"шестнадцатьs";"семнадцатьs";"восемнадцатьs";"девятнадцатьs")
&"миллиардов "
;
ВЫБОР(ПСТР(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));12);3)
;2;1)+1
;"";"";"двадцатьs";"тридцатьs";"сорокs";
"пятьдесятs";"шестьдесятs";"семьдесятs";"восемьдесятs";"девяностоs")
&
ВЫБОР(ПРАВСИМВ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));12);3)
)+1
;"миллиардов ";"одинsмиллиард ";"дваsмиллиарда ";"триsмиллиарда ";"четыреsмиллиарда ";
"пятьsмиллиардов ";"шестьsмиллиардов ";"семьsмиллиардов ";"восемьsмиллиардов ";"девятьsмиллиардов ")
))
&
ЕСЛИ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));9);3)
+0=0;""
;
ВЫБОР(ЛЕВСИМВ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));9);3)
)+1
;"";"стоs";"двестиs";"тристаs";"четырестаs";
"пятьсотs";"шестьсотs";"семьсотs";"восемьсотs";"девятьсотs")
&
ЕСЛИ(ПСТР(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));9);3)
;2;1)+0=1;
ВЫБОР(ПРАВСИМВ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));9);3)
;2)-9
;"десятьs";"одиннадцатьs";"двенадцатьs";"тринадцатьs";"четырнадцатьs";
"пятнадцатьs";"шестнадцатьs";"семнадцатьs";"восемнадцатьs";"девятнадцатьs")
&"миллионов "
;
ВЫБОР(ПСТР(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));9);3)
;2;1)+1
;"";"";"двадцатьs";"тридцатьs";"сорокs";
"пятьдесятs";"шестьдесятs";"семьдесятs";"восемьдесятs";"девяностоs")
&
ВЫБОР(ПРАВСИМВ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));9);3)
)+1
;"миллионов ";"одинsмиллион ";"дваsмиллиона ";"триsмиллиона ";"четыреsмиллиона ";
"пятьsмиллионов ";"шестьsмиллионов ";"семьsмиллионов ";"восемьsмиллионов ";"девятьsмиллионов ")
))
&
ЕСЛИ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));6);3)
+0=0;""
;
ВЫБОР(ЛЕВСИМВ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));6);3)
)+1
;"";"стоs";"двестиs";"тристаs";"четырестаs";
"пятьсотs";"шестьсотs";"семьсотs";"восемьсотs";"девятьсотs")
&
ЕСЛИ(ПСТР(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));6);3)
;2;1)+0=1;
ВЫБОР(ПРАВСИМВ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));6);3)
;2)-9
;"десятьs";"одиннадцатьs";"двенадцатьs";"тринадцатьs";"четырнадцатьs";
"пятнадцатьs";"шестнадцатьs";"семнадцатьs";"восемнадцатьs";"девятнадцатьs")
&"тысяч "
;
ВЫБОР(ПСТР(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));6);3)
;2;1)+1
;"";"";"двадцатьs";"тридцатьs";"сорокs";
"пятьдесятs";"шестьдесятs";"семьдесятs";"восемьдесятs";"девяностоs")
&
ВЫБОР(ПРАВСИМВ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));6);3)
)+1
;"тысяч ";"однаsтысяча ";"двеsтысячи ";"триsтысячи ";"четыреsтысячи ";
"пятьsтысяч ";"шестьsтысяч ";"семьsтысяч ";"восемьsтысяч ";"девятьsтысяч ")
))
&
ЕСЛИ(
ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));3)
+0=0;ЕСЛИ(ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2))=0;"нольsрублей";"sрублей")
;
ВЫБОР(ЛЕВСИМВ(
ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));3)
)+1
;"";"стоs";"двестиs";"тристаs";"четырестаs";
"пятьсотs";"шестьсотs";"семьсотs";"восемьсотs";"девятьсотs")
&
ЕСЛИ(ПСТР(
ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));3)
;2;1)+0=1;
ВЫБОР(ПРАВСИМВ(
ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));3)
;2)-9
;"десятьs";"одиннадцатьs";"двенадцатьs";"тринадцатьs";"четырнадцатьs";
"пятнадцатьs";"шестнадцатьs";"семнадцатьs";"восемнадцатьs";"девятнадцатьs")
&"рублей"
;
ВЫБОР(ПСТР(
ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));3)
;2;1)+1
;"";"";"двадцатьs";"тридцатьs";"сорокs";
"пятьдесятs";"шестьдесятs";"семьдесятs";"восемьдесятs";"девяностоs")
&
ВЫБОР(ПРАВСИМВ(
ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));3)
)+1
;"";ВЫБОР(1;"одинs";"однаs";"одноs");
ВЫБОР(1;"дваs";"двеs";"дваs");"триs";"четыреs";
"пятьs";"шестьs";"семьs";"восемьs";"девятьs")
&
ВЫБОР(ВЫБОР(ПРАВСИМВ(
ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));3)
)+1;3;1;2;2;2;3;3;3;3;3);
"рубль";"рубля";"рублей")
))
);"s";" ");"S";"")
&
" " & ПРАВСИМВ(
ПРАВСИМВ(ПОВТОР("0";(ЦЕЛОЕ(2/3)+1)*3)&ОКРУГЛ((ОКРУГЛ(ABS(A1);2)-ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2)))*10^2;0);(ЦЕЛОЕ(2/3)+1)*3)
;2)&" "
&
ЕСЛИ(ИЛИ(ПРАВСИМВ(
ПРАВСИМВ(ПОВТОР("0";(ЦЕЛОЕ(2/3)+1)*3)&ОКРУГЛ((ОКРУГЛ(ABS(A1);2)-ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2)))*10^2;0);(ЦЕЛОЕ(2/3)+1)*3)
;3)+0=0;ПСТР(ПРАВСИМВ(
ПРАВСИМВ(ПОВТОР("0";(ЦЕЛОЕ(2/3)+1)*3)&ОКРУГЛ((ОКРУГЛ(ABS(A1);2)-ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2)))*10^2;0);(ЦЕЛОЕ(2/3)+1)*3)
;3);2;1)+0=1);
"копеек";
ВЫБОР(ВЫБОР(ПРАВСИМВ(ПРАВСИМВ(
ПРАВСИМВ(ПОВТОР("0";(ЦЕЛОЕ(2/3)+1)*3)&ОКРУГЛ((ОКРУГЛ(ABS(A1);2)-ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2)))*10^2;0);(ЦЕЛОЕ(2/3)+1)*3)
;3))+1;3;1;2;2;2;3;3;3;3;3);
"копейка";"копейки";"копеек")
)
В отличие от вышеприведенных метода и функции, "проговаривающих" только целые числа, эта формула содержит еще и блок копеек, т.е. дробной части. Формула также приводится в прилагаемом файле, чтобы владельцы копий Excel, отличных от русской, либо русской, но с иными разделителями, тоже могли ею воспользоваться. 

Буковка "s" на конце числительных символизирует "пробел" ("space") и предназначена для корректной работы функции ПРОПНАЧ по переводу в верхний регистр только первой буквы каждой триады (иначе, в случае настоящего пробела, прописной стала бы первая буква в каждом слове).

Верхняя граница действия формулы - 999 триллионов (15-тизначное число). Если такие гигантские суммы не предполагается прописывать словами в повседневной хозяйственной практике, то формулу можно подрезать, удалив из нее, скажем, блоки триллионов и миллиардов, ограничив, таким образом, ее значением суммы в 999 миллионов, а то и 999 тысяч, если удалить еще и блок миллионов.

Для облегчения ориентирования в тексте формулы - блок триллионов ограничен фрагментами (фрагменты входят в состав блока):

ЕСЛИ(
ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));15);3)
+0=0;""
;
.......................................................................
.......................................................................
.......................................................................
;"триллионов ";"одинsтриллион ";"дваsтриллиона ";"триsтриллиона ";"четыреsтриллиона ";
"пятьsтриллионов ";"шестьsтриллионов ";"семьsтриллионов ";"восемьsтриллионов ";"девятьsтриллионов ")
))
&
Блоки остальных разрядностей можно найти по аналогии. Дополнительным средством идентификации блока триллионов может быть второе число 15 в характерной подстроке ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));15);3). Для миллиардов это число будет 12, для миллионов - 9, для тысяч - 6 (думаю, комментарии излишни).

Конечно же, с вычислительной точки зрения мегаформула (назовем ее "алгоритмом в одной ячейке") вопиюще неоптимальна. Невооруженным глазом видны многократно повторяющиеся одинаковые фрагменты. И всё в угоду тому, чтобы содержать ссылки на одну единственную ячейку A1 - и тем самым производить впечатление экспоната кунсткамеры. Но, согласитесь, прикольно! А современные компы потянут и не такие вычисления ;)

Если же количество задействованных для расчета суммы прописью ячеек для нас не имеет значения (в разумных пределах), то можно использовать "алгоритм в нескольких ячейках". Общее количество символов во всех используемых при этом формулах будет существенно меньше и в самой "насыщенной" не превысит значения 1024, что позволит применить "алгоритм в нескольких ячейках" также и в более ранних версиях Excel (до 2007).

Ниже введем на рабочем листе Excel несколько формул, являющихся составными частями алгоритма в нескольких ячейках. Общее количество задействованных ячеек - 12. Можно уменьшить до 10, если объединить в одной ячейке "рубли", "копейки" и "сцепить" (если только не актуально ограничение на длину формулы в 1024 символа). И дальше - уменьшение на 2 ячейки при каждом отказе от использования старших разрядов (триллионов, миллиардов и т.д.). Для сумм меньше миллиона можно будет уложиться в 4 ячейки.

Итак, формула для ячеек B1:F1 - в них будут отображаться триады цифрами (диапазон содержит 5 ячеек):

=ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР("0";15)&ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));{15;12;9;6;3});3)
Эта формула массива должна быть введена при помощи следующих шагов: копируем ее текст отсюда; в Excel выделяем диапазон B1:F1; жмем F2 для перехода в режим редактирования; в строке редактирования делаем вставку из буфера; завершаем ввод нажатием комбинации Shift+Ctrl+Enter.

Для ячеек G1:J1 - триады словами (4 ячейки!):

=ПОДСТАВИТЬ(ПРОПНАЧ(
ЕСЛИ(B1:E1+0=0
;
""
;
ВЫБОР(ЛЕВСИМВ(B1:E1)+1;
"";"стоs";"двестиs";"тристаs";"четырестаs";
"пятьсотs";"шестьсотs";"семьсотs";"восемьсотs";"девятьсотs")
&
ЕСЛИ(ПСТР(B1:E1;2;1)+0=1
;
ВЫБОР(ПРАВСИМВ(B1:E1;2)-9;
"десятьs";"одиннадцатьs";"двенадцатьs";"тринадцатьs";"четырнадцатьs";
"пятнадцатьs";"шестнадцатьs";"семнадцатьs";"восемнадцатьs";"девятнадцатьs")
&{"триллионов ";"миллиардов ";"миллионов ";"тысяч "}
;
ВЫБОР(ПСТР(B1:E1;2;1)+1;
"";"";"двадцатьs";"тридцатьs";"сорокs";
"пятьдесятs";"шестьдесятs";"семьдесятs";"восемьдесятs";"девяностоs")
&
ВЫБОР(ПРАВСИМВ(B1:E1)+1;
"";ВЫБОР({1;1;1;1};"одинs";"однаs";"одноs");
ВЫБОР({1;1;1;2};"дваs";"двеs";"дваs");"триs";"четыреs";
"пятьs";"шестьs";"семьs";"восемьs";"девятьs")
&
{"триллион";"миллиард";"миллион";"тысяч"}&
ВЫБОР(ВЫБОР(ПРАВСИМВ(B1:E1)+1;3;1;2;2;2;3;3;3;3;3);
{" ";" ";" ";"а "};{"а ";"а ";"а ";"и "};{"ов ";"ов ";"ов ";" "})
))
);"s";" ")
Эта тоже формула массива, которая вводится тем же способом, что и предыдущая. Следует обратить внимание, что эта формула будет занимать 4 ячейки, а не 5 как предыдущая. 5-ю ячейку (рублей) мы введем отдельно (почему - будет понятно далее из файла, при рассмотрении параметрического варианта алгоритма).

Для ячейки K1 - рубли (обычная формула - ввод завершается простым нажатием Enter):

=ПОДСТАВИТЬ(ПОДСТАВИТЬ(ПРОПНАЧ(
ЕСЛИ(F1+0=0
;
ЕСЛИ(ЦЕЛОЕ(ABS(A1))=0;"нольsрублей";"sрублей")
;
ВЫБОР(ЛЕВСИМВ(F1)+1;
"";"стоs";"двестиs";"тристаs";"четырестаs";
"пятьсотs";"шестьсотs";"семьсотs";"восемьсотs";"девятьсотs")
&
ЕСЛИ(ПСТР(F1;2;1)+0=1
;
ВЫБОР(ПРАВСИМВ(F1;2)-9;
"десятьs";"одиннадцатьs";"двенадцатьs";"тринадцатьs";"четырнадцатьs";
"пятнадцатьs";"шестнадцатьs";"семнадцатьs";"восемнадцатьs";"девятнадцатьs")
&"рублей"
;
ВЫБОР(ПСТР(F1;2;1)+1;
"";"";"двадцатьs";"тридцатьs";"сорокs";
"пятьдесятs";"шестьдесятs";"семьдесятs";"восемьдесятs";"девяностоs")
&
ВЫБОР(ПРАВСИМВ(F1)+1;
"";ВЫБОР(1;"одинs";"однаs";"одноs");
ВЫБОР(1;"дваs";"двеs";"дваs");"триs";"четыреs";
"пятьs";"шестьs";"семьs";"восемьs";"девятьs")
&
ВЫБОР(ВЫБОР(ПРАВСИМВ(F1)+1;3;1;2;2;2;3;3;3;3;3);
"рубль";"рубля";"рублей")
))
);"s";" ");"S";"")
 


Для ячейки L1 - копейки (обычная формула - ввод завершается простым нажатием Enter):

=" " & ПРАВСИМВ(
ПРАВСИМВ(ПОВТОР("0";(ЦЕЛОЕ(2/3)+1)*3)&ОКРУГЛ((ОКРУГЛ(ABS(A1);2)-ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2)))*10^2;0);(ЦЕЛОЕ(2/3)+1)*3)
;2)&" "
&
ЕСЛИ(ИЛИ(ПРАВСИМВ(
ПРАВСИМВ(ПОВТОР("0";(ЦЕЛОЕ(2/3)+1)*3)&ОКРУГЛ((ОКРУГЛ(ABS(A1);2)-ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2)))*10^2;0);(ЦЕЛОЕ(2/3)+1)*3)
;3)+0=0;ПСТР(ПРАВСИМВ(
ПРАВСИМВ(ПОВТОР("0";(ЦЕЛОЕ(2/3)+1)*3)&ОКРУГЛ((ОКРУГЛ(ABS(A1);2)-ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2)))*10^2;0);(ЦЕЛОЕ(2/3)+1)*3)
;3);2;1)+0=1);
"копеек";
ВЫБОР(ВЫБОР(ПРАВСИМВ(ПРАВСИМВ(
ПРАВСИМВ(ПОВТОР("0";(ЦЕЛОЕ(2/3)+1)*3)&ОКРУГЛ((ОКРУГЛ(ABS(A1);2)-ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2)))*10^2;0);(ЦЕЛОЕ(2/3)+1)*3)
;3))+1;3;1;2;2;2;3;3;3;3;3);
"копейка";"копейки";"копеек")
)
Возможно, вы заметили, что в этой формуле (и в других) присутствуют фрагменты, которые могли бы быть вычислены заранее и введены в формулу как константы, например: (ЦЕЛОЕ(2/3)+1)*3) или 10^2. Это сделано сознательно - в таком виде легче конструировать универсальные параметрические формулы на случаи любых единиц измерения, а не только рублей и копеек. В параметрическом варианте (см. в файле) число 2 в этих примерах, которое по сути представляет собой кол-во знаков дробной части, заменено ссылкой на ячейку, в которой хранится данная величина.

Наконец, в ячейке M1 получаем окончательную сумму прописью:

=СЦЕПИТЬ(G1;H1;I1;J1;K1;L1)

// удивительно упорство Microsoft по непредоставлению до сих пор возможности записи этой формулы в виде =СЦЕПИТЬ(G1:L1)
 

Простую формулу в ячейке M1 можно усложнить дополнительной обработкой по желанию, например, оставив заглавной только самую первую букву фразы:

=ЛЕВСИМВ(СЦЕПИТЬ(G1;H1;I1;J1;K1;L1))&СТРОЧН(ПСТР(СЦЕПИТЬ(G1;H1;I1;J1;K1;L1);2;1000))

// здесь 1000 - произвольное, заведомо большее длины строки число
 

В прилагаемом файле на основе приведенных формул алгоритма в нескольких ячейках демонстрируется также его параметрический вариант - на случай любых единиц измерения. Настройка на любые единицы выполняется путем указания необходимых параметров (род единиц, формы считаемых существительных, кол-во знаков дробной части) в дополнительных ячейках. С несложными деталями можно ознакомиться самостоятельно. Файл создан в Excel 2010.]]></description>
			<content:encoded><![CDATA[<div>Это сообщение готовилось к публикации почти два года тому назад. Тогда мне потребовалась сумма прописью в своей разработке. Прошерстив форум, нашёл ссылки на класс RNumDateInWordConverter, но что-то с первого раза с этим классом не получилось. Потом, правда, срослось, но &quot;осадок остался&quot; и появилось горячее желание прикоснуться к теме лично, тем более, что в голове сложился своеобразный алгоритм решения (как мне кажется... или это только мне кажется его своеобразность? :)). Этим алгоритмом мне тогда и хотелось поделиться, но текучка отложила этот радостный момент на неопределенный срок.<br />
<br />
Предлагаемый алгоритм основывается на переводе исходного числа (его целой части) в последовательность триад и в обработке каждой триады как числа от 0 до 999. При этом к соответствующей разрядности типа &quot;тысяч&quot;, &quot;миллионов&quot;, &quot;миллиардов&quot; относимся так же, как к любому считаемому существительному, например, к &quot;рублям&quot; или иным единицам (метрам, штукам и т.п.). Это позволяет вынести алгоритм обработки триады в отдельную, достаточно компактную функцию, снаружи которой процесс сводится к нескольким ее вызовам с соответствующей считаемой единицей в качестве параметра.<br />
<br />
С деталями алгоритма можно ознакомиться по приведенным ниже текстам метода sumInWords_RU (для Аксапты) и одноименной же функции (для VBA).<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: green">// KKu, 23.04.2010 --&gt; РУССКАЯ СУММА ПРОПИСЬЮ
</span>
<span style="color: green">//  _sourceReal - вещественное число для прописи (минус и дробная часть игнорируются при обработке)
</span><span style="color: green">//  _unit1,_unit2,_unit5 - формы считаемого существительного соответственно для 1 единицы, 2 ед-ц и 5 ед-ц
</span><span style="color: green">//  _gender - код рода считаемого существительного = 1-мужской, 2-женский, 3-средний
</span><span style="color: green">//  _capital - в возвращаемой строке делать большими буквы:
</span><span style="color: green">//      0-всё маленькими, 1-только самую первую букву всей строки,2-первая буква каждой триады
</span>
<span style="color: blue">static</span> <span style="color: blue">str</span> sumInWords_RU( <span style="color: blue">real</span>  _sourceReal,
                          <span style="color: blue">str</span>   _unit1   = <span style="color: red">'рубль'</span>,  <span style="color: green">// (один) рубль
</span>                          <span style="color: blue">str</span>   _unit2   = <span style="color: red">'рубля'</span>,  <span style="color: green">// (два ) рубля
</span>                          <span style="color: blue">str</span>   _unit5   = <span style="color: red">'рублей'</span>, <span style="color: green">// (пять) рублей
</span>                          <span style="color: blue">int</span>   _gender  = 1,        <span style="color: green">// 1 - мужской (рубль)
</span>                          <span style="color: blue">int</span>   _capital = 1 )       <span style="color: green">// 1 - только первая всей строки
</span>{
    <span style="color: blue">str</span>         strSource = strFmt(<span style="color: red">'00%1'</span>, num2str( trunc((abs(_sourceReal))),1,0,1,0 ));
    <span style="color: blue">int</span>         cntTriad  = trunc(strLen(strSource)/3);
    <span style="color: blue">str</span>         urrTriad, morePwr12, fRet;
    <span style="color: blue">int</span>         i;

    <span style="color: blue">str</span> triadWords(<span style="color: blue">str</span> triad, <span style="color: blue">str</span> unit1, <span style="color: blue">str</span> unit2, <span style="color: blue">str</span> unit5, <span style="color: blue">int</span> gender)
    {
        <span style="color: blue">str</span> ret  = conPeek([<span style="color: red">''</span>,<span style="color: red">'сто '</span>,<span style="color: red">'двести '</span>,<span style="color: red">'триста '</span>,<span style="color: red">'четыреста '</span>,
                    <span style="color: red">'пятьсот '</span>,<span style="color: red">'шестьсот '</span>,<span style="color: red">'семьсот '</span>,<span style="color: red">'восемьсот '</span>,<span style="color: red">'девятьсот '</span>],
                    str2int(subStr(triad,1,1))+1 );
        <span style="color: blue">str</span> currUnit = unit5;
        ;
        <span style="color: blue">if</span> (strSource==<span style="color: red">'000'</span>)
        {
            ret = <span style="color: red">'ноль '</span>;
        }
        <span style="color: blue">else</span> <span style="color: blue">if</span> (subStr(triad,2,1)==<span style="color: red">'1'</span>)
        {
            ret += conPeek([<span style="color: red">'десять'</span>,<span style="color: red">'одиннадцать'</span>,<span style="color: red">'двенадцать'</span>,<span style="color: red">'тринадцать'</span>,<span style="color: red">'четырнадцать'</span>,
                    <span style="color: red">'пятнадцать'</span>,<span style="color: red">'шестнадцать'</span>,<span style="color: red">'семнадцать'</span>,<span style="color: red">'восемнадцать'</span>,<span style="color: red">'девятнадцать'</span>],
                    str2int(subStr(triad,2,2))-9 ) + <span style="color: red">' '</span>;
        }
        <span style="color: blue">else</span>
        {
            ret += conPeek([<span style="color: red">''</span>,<span style="color: red">''</span>,<span style="color: red">'двадцать '</span>,<span style="color: red">'тридцать '</span>,<span style="color: red">'сорок '</span>,
                    <span style="color: red">'пятьдесят '</span>,<span style="color: red">'шестьдесят '</span>,<span style="color: red">'семьдесят '</span>,<span style="color: red">'восемьдесят '</span>,<span style="color: red">'девяносто '</span>],
                    str2int(subStr(triad,2,1))+1 );

            ret += conPeek([<span style="color: red">''</span>,conPeek([<span style="color: red">'один '</span>,<span style="color: red">'одна '</span>,<span style="color: red">'одно '</span>],gender),
                    conPeek([<span style="color: red">'два '</span> ,<span style="color: red">'две '</span> ,<span style="color: red">'два '</span> ],gender),
                    <span style="color: red">'три '</span>,<span style="color: red">'четыре '</span>,<span style="color: red">'пять '</span>,<span style="color: red">'шесть '</span>,<span style="color: red">'семь '</span>,<span style="color: red">'восемь '</span>,<span style="color: red">'девять '</span>],
                    str2int(subStr(triad,3,1))+1 );

            currUnit = conPeek([unit5, unit1, unit2,unit2,unit2, unit5,unit5,unit5,unit5,unit5],
                        str2int(subStr(triad,3,1))+1 );
        }
        <span style="color: blue">return</span> ret ? strFmt(<span style="color: red">'%1%2 '</span>, str2Capital(ret), currUnit) : (cntTriad-i+1)==1 ? currUnit : <span style="color: red">''</span>;
    }
    ;

    strSource = subStr(strSource, strLen(strSource)-cntTriad*3+1, cntTriad*3);
    <span style="color: blue">for</span> (i=1; i&lt;=cntTriad; i++)
    {
        urrTriad = subStr(strSource, (i-1)*3+1, 3);
        <span style="color: blue">switch</span> (cntTriad-i+1)
        {
            <span style="color: blue">case</span> 1: fRet += triadWords(urrTriad,_unit1    ,_unit2     ,_unit5,_gender); <span style="color: blue">break</span>; <span style="color: green">// 10^0
</span>            <span style="color: blue">case</span> 2: fRet += triadWords(urrTriad,<span style="color: red">'тысяча'</span>  ,<span style="color: red">'тысячи'</span>   ,<span style="color: red">'тысяч'</span>     ,2); <span style="color: blue">break</span>; <span style="color: green">// 10^3
</span>            <span style="color: blue">case</span> 3: fRet += triadWords(urrTriad,<span style="color: red">'миллион'</span> ,<span style="color: red">'миллиона'</span> ,<span style="color: red">'миллионов'</span> ,1); <span style="color: blue">break</span>; <span style="color: green">// 10^6
</span>            <span style="color: blue">case</span> 4: fRet += triadWords(urrTriad,<span style="color: red">'миллиард'</span>,<span style="color: red">'миллиарда'</span>,<span style="color: red">'миллиардов'</span>,1); <span style="color: blue">break</span>; <span style="color: green">// 10^9
</span>            <span style="color: blue">case</span> 5: fRet += triadWords(urrTriad,<span style="color: red">'триллион'</span>,<span style="color: red">'триллиона'</span>,<span style="color: red">'триллионов'</span>,1); <span style="color: blue">break</span>; <span style="color: green">// 10^12
</span>            <span style="color: blue">default</span>:
                    morePwr12 = strFmt(<span style="color: red">'10^%1'</span>,(cntTriad-i)*3); <span style="color: green">// 10^15 и т.д.
</span>                    fRet += triadWords(urrTriad, morePwr12, morePwr12 , morePwr12  ,1);
        }
    }
    <span style="color: blue">return</span> strRTrim(conPeek([strLwr(fRet), str2Capital(strLwr(fRet)), fRet], _capital+1));
}</pre></div>VBA:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: red">''</span><span style="color: green">//РУССКАЯ СУММА ПРОПИСЬЮ - функция sumInWords_RU
</span>Option Explicit

Dim strSource   As String
Dim cntTriad    As Integer
Dim i           As Integer

<span style="color: blue">Private</span> Function triadWords(ByVal triad As String, _
                            ByVal unit1 As String, _
                            ByVal unit2 As String, _
                            ByVal unit5 As String, _
                            ByVal gender As Integer) As String
                            
    Dim ret         As String
    Dim currUnit    As String
    
    ret = Choose(CInt(<span style="color: blue">Left</span>(triad, 1)) + 1, <span style="color: red">&quot;&quot;</span>, <span style="color: red">&quot;сто &quot;</span>, <span style="color: red">&quot;двести &quot;</span>, <span style="color: red">&quot;триста &quot;</span>, <span style="color: red">&quot;четыреста &quot;</span>, _
                <span style="color: red">&quot;пятьсот &quot;</span>, <span style="color: red">&quot;шестьсот &quot;</span>, <span style="color: red">&quot;семьсот &quot;</span>, <span style="color: red">&quot;восемьсот &quot;</span>, <span style="color: red">&quot;девятьсот &quot;</span>)
    currUnit = unit5
    
    <span style="color: blue">If</span> strSource = <span style="color: red">&quot;000&quot;</span> Then
        ret = <span style="color: red">&quot;ноль &quot;</span>
        
    ElseIf Mid(triad, 2, 1) = <span style="color: red">&quot;1&quot;</span> Then
        ret = ret &amp; Choose(CInt(<span style="color: blue">Right</span>(triad, 2)) - 9, <span style="color: red">&quot;десять&quot;</span>, <span style="color: red">&quot;одиннадцать&quot;</span>, _
                    <span style="color: red">&quot;двенадцать&quot;</span>, <span style="color: red">&quot;тринадцать&quot;</span>, <span style="color: red">&quot;четырнадцать&quot;</span>, <span style="color: red">&quot;пятнадцать&quot;</span>, _
                    <span style="color: red">&quot;шестнадцать&quot;</span>, <span style="color: red">&quot;семнадцать&quot;</span>, <span style="color: red">&quot;восемнадцать&quot;</span>, <span style="color: red">&quot;девятнадцать&quot;</span>) &amp; <span style="color: red">&quot; &quot;</span>
    <span style="color: blue">Else</span>
        ret = ret &amp; Choose(CInt(Mid(triad, 2, 1)) + 1, <span style="color: red">&quot;&quot;</span>, <span style="color: red">&quot;&quot;</span>, _
                    <span style="color: red">&quot;двадцать &quot;</span>, <span style="color: red">&quot;тридцать &quot;</span>, <span style="color: red">&quot;сорок &quot;</span>, <span style="color: red">&quot;пятьдесят &quot;</span>, _
                    <span style="color: red">&quot;шестьдесят &quot;</span>, <span style="color: red">&quot;семьдесят &quot;</span>, <span style="color: red">&quot;восемьдесят &quot;</span>, <span style="color: red">&quot;девяносто &quot;</span>)
        ret = ret &amp; Choose(CInt(<span style="color: blue">Right</span>(triad, 1)) + 1, <span style="color: red">&quot;&quot;</span>, _
                    Choose(gender, <span style="color: red">&quot;один&quot;</span>, <span style="color: red">&quot;одна&quot;</span>, <span style="color: red">&quot;одно&quot;</span>) &amp; <span style="color: red">&quot; &quot;</span>, _
                    Choose(gender, <span style="color: red">&quot;два&quot;</span>, <span style="color: red">&quot;две&quot;</span>, <span style="color: red">&quot;два&quot;</span>) &amp; <span style="color: red">&quot; &quot;</span>, _
                    <span style="color: red">&quot;три &quot;</span>, <span style="color: red">&quot;четыре &quot;</span>, <span style="color: red">&quot;пять &quot;</span>, <span style="color: red">&quot;шесть &quot;</span>, <span style="color: red">&quot;семь &quot;</span>, <span style="color: red">&quot;восемь &quot;</span>, <span style="color: red">&quot;девять &quot;</span>)
        currUnit = Choose(CInt(<span style="color: blue">Right</span>(triad, 1)) + 1, _
                    unit5, unit1, unit2, unit2, unit2, _
                    unit5, unit5, unit5, unit5, unit5)
    End <span style="color: blue">If</span>
    
    triadWords = IIf(ret &lt;&gt; <span style="color: red">&quot;&quot;</span>, UCase(<span style="color: blue">Left</span>(ret, 1)) &amp; Mid(ret, 2) &amp; currUnit &amp; <span style="color: red">&quot; &quot;</span>, _
                                IIf((cntTriad - i + 1) = 1, currUnit, <span style="color: red">&quot;&quot;</span>))
End Function

<span style="color: red">''</span><span style="color: green">//РУССКАЯ СУММА ПРОПИСЬЮ
</span><span style="color: red">''</span><span style="color: green">//  sourceReal - вещественное число для прописи (минус и дробная часть игнорируются при обработке)
</span><span style="color: red">''</span><span style="color: green">//  unit1, unit2, unit5 - формы считаемого существительного соответственно для 1 единицы, 2 ед-ц и 5 ед-ц
</span><span style="color: red">''</span><span style="color: green">//  gender - код рода считаемого существительного = 1-мужской, 2-женский, 3-средний
</span><span style="color: red">''</span><span style="color: green">//  capital - в возвращаемой строке делать большими буквы:
</span><span style="color: red">''</span><span style="color: green">//     0-всё маленькими, 1-только самую первую букву всей строки,2-первая буква каждой триады
</span><span style="color: blue">Public</span> Function sumInWords_RU(ByVal sourceReal As Double, _
                              Optional ByVal unit1 As String = <span style="color: red">&quot;рубль&quot;</span>, _
                              Optional ByVal unit2 As String = <span style="color: red">&quot;рубля&quot;</span>, _
                              Optional ByVal unit5 As String = <span style="color: red">&quot;рублей&quot;</span>, _
                              Optional ByVal gender As Integer = 1, _
                              Optional ByVal capital As Integer = 1) As String

    Dim urTrd      As String
    Dim morePwr12   As String
    Dim fRet        As String
   
    strSource = <span style="color: red">&quot;00&quot;</span> &amp; Format(<span style="color: blue">Int</span>(Abs(sourceReal)), <span style="color: red">&quot;0&quot;</span>)
    cntTriad = <span style="color: blue">Int</span>(Len(strSource) / 3)
    strSource = <span style="color: blue">Right</span>(strSource, cntTriad * 3)
    
    <span style="color: blue">For</span> i = 1 To cntTriad
        urTrd = Mid(strSource, (i - 1) * 3 + 1, 3)
        <span style="color: blue">Select</span> <span style="color: blue">Case</span> cntTriad - i + 1
            <span style="color: blue">Case</span> 1: fRet = fRet &amp; triadWords(urTrd, unit1, unit2, unit5, gender)
            <span style="color: blue">Case</span> 2: fRet = fRet &amp; triadWords(urTrd, <span style="color: red">&quot;тысяча&quot;</span>, <span style="color: red">&quot;тысячи&quot;</span>, <span style="color: red">&quot;тысяч&quot;</span>, 2)
            <span style="color: blue">Case</span> 3: fRet = fRet &amp; triadWords(urTrd, <span style="color: red">&quot;миллион&quot;</span>, <span style="color: red">&quot;миллиона&quot;</span>, <span style="color: red">&quot;миллионов&quot;</span>, 1)
            <span style="color: blue">Case</span> 4: fRet = fRet &amp; triadWords(urTrd, <span style="color: red">&quot;миллиард&quot;</span>, <span style="color: red">&quot;миллиарда&quot;</span>, <span style="color: red">&quot;миллиардов&quot;</span>, 1)
            <span style="color: blue">Case</span> 5: fRet = fRet &amp; triadWords(urTrd, <span style="color: red">&quot;триллион&quot;</span>, <span style="color: red">&quot;триллиона&quot;</span>, <span style="color: red">&quot;триллионов&quot;</span>, 1)
            <span style="color: blue">Case</span> <span style="color: blue">Else</span>
                morePwr12 = <span style="color: red">&quot;10^&quot;</span> &amp; CStr(cntTriad - i) * 3
                fRet = fRet &amp; triadWords(urTrd, morePwr12, morePwr12, morePwr12, 1)
        End <span style="color: blue">Select</span>
    <span style="color: blue">Next</span> i
    
    sumInWords_RU = RTrim(Choose(capital + 1, LCase(fRet), <span style="color: blue">Left</span>(fRet, 1) &amp; LCase(Mid(fRet, 2)), fRet))
End Function</pre></div>Но что же заставило меня наконец перевести это сообщение из статуса &quot;черновик&quot; в открытый доступ? А вот что - на основе ранее разработанных вышеприведенных функций сочинилась формула для Excel, помещающаяся в одной ячейке! Т.е. в ячейку А1 вводим число, в ячейку B1 - формулу и в ней же читаем сумму прописью. Никаких макросов, весь текст собирается в одной ячейке с использованием стандартных функций рабочего листа (у кого-нибудь есть образец заявки в книгу рекордов Гиннесса? :)) <br />
<br />
По терминологии маэстро <a href="http://spreadsheetpage.com/" target="_blank">Дж.Уокенбаха</a> эта формула - мегаформула (не в смысле, что такая крутая, а потому что без промежуточных результатов). С количеством символов около 6 тысяч - поэтому может использоваться только в версии Excel, начиная с 2007, когда допустимая длина формулы увеличилась с 1024 до 8192 символов. Для более ранних версий, однако, возможен &quot;расчет&quot; суммы прописью с задействованием нескольких соседних ячеек (и существенным сокращением общего кол-ва формульных символов за счет использования формул массива). Но об этом позже, а сейчас - вот эта базовая формулища:<br />
<br />
МЕГАФОРМУЛА ДЛЯ ОДНОЙ ЯЧЕЙКИ EXCEL:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">=(((
(
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));15);3)
+0=0;<span style="color: red">&quot;&quot;</span>
;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));15);3)
)+1
;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;стоs&quot;</span>;<span style="color: red">&quot;двестиs&quot;</span>;<span style="color: red">&quot;тристаs&quot;</span>;<span style="color: red">&quot;четырестаs&quot;</span>;
<span style="color: red">&quot;пятьсотs&quot;</span>;<span style="color: red">&quot;шестьсотs&quot;</span>;<span style="color: red">&quot;семьсотs&quot;</span>;<span style="color: red">&quot;восемьсотs&quot;</span>;<span style="color: red">&quot;девятьсотs&quot;</span>)
&amp;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));15);3)
;2;1)+0=1;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));15);3)
;2)-9
;<span style="color: red">&quot;десятьs&quot;</span>;<span style="color: red">&quot;одиннадцатьs&quot;</span>;<span style="color: red">&quot;двенадцатьs&quot;</span>;<span style="color: red">&quot;тринадцатьs&quot;</span>;<span style="color: red">&quot;четырнадцатьs&quot;</span>;
<span style="color: red">&quot;пятнадцатьs&quot;</span>;<span style="color: red">&quot;шестнадцатьs&quot;</span>;<span style="color: red">&quot;семнадцатьs&quot;</span>;<span style="color: red">&quot;восемнадцатьs&quot;</span>;<span style="color: red">&quot;девятнадцатьs&quot;</span>)
&amp;<span style="color: red">&quot;триллионов &quot;</span>
;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));15);3)
;2;1)+1
;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;двадцатьs&quot;</span>;<span style="color: red">&quot;тридцатьs&quot;</span>;<span style="color: red">&quot;сорокs&quot;</span>;
<span style="color: red">&quot;пятьдесятs&quot;</span>;<span style="color: red">&quot;шестьдесятs&quot;</span>;<span style="color: red">&quot;семьдесятs&quot;</span>;<span style="color: red">&quot;восемьдесятs&quot;</span>;<span style="color: red">&quot;девяностоs&quot;</span>)
&amp;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));15);3)
)+1
;<span style="color: red">&quot;триллионов &quot;</span>;<span style="color: red">&quot;одинsтриллион &quot;</span>;<span style="color: red">&quot;дваsтриллиона &quot;</span>;<span style="color: red">&quot;триsтриллиона &quot;</span>;<span style="color: red">&quot;четыреsтриллиона &quot;</span>;
<span style="color: red">&quot;пятьsтриллионов &quot;</span>;<span style="color: red">&quot;шестьsтриллионов &quot;</span>;<span style="color: red">&quot;семьsтриллионов &quot;</span>;<span style="color: red">&quot;восемьsтриллионов &quot;</span>;<span style="color: red">&quot;девятьsтриллионов &quot;</span>)
))
&amp;
(
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));12);3)
+0=0;<span style="color: red">&quot;&quot;</span>
;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));12);3)
)+1
;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;стоs&quot;</span>;<span style="color: red">&quot;двестиs&quot;</span>;<span style="color: red">&quot;тристаs&quot;</span>;<span style="color: red">&quot;четырестаs&quot;</span>;
<span style="color: red">&quot;пятьсотs&quot;</span>;<span style="color: red">&quot;шестьсотs&quot;</span>;<span style="color: red">&quot;семьсотs&quot;</span>;<span style="color: red">&quot;восемьсотs&quot;</span>;<span style="color: red">&quot;девятьсотs&quot;</span>)
&amp;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));12);3)
;2;1)+0=1;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));12);3)
;2)-9
;<span style="color: red">&quot;десятьs&quot;</span>;<span style="color: red">&quot;одиннадцатьs&quot;</span>;<span style="color: red">&quot;двенадцатьs&quot;</span>;<span style="color: red">&quot;тринадцатьs&quot;</span>;<span style="color: red">&quot;четырнадцатьs&quot;</span>;
<span style="color: red">&quot;пятнадцатьs&quot;</span>;<span style="color: red">&quot;шестнадцатьs&quot;</span>;<span style="color: red">&quot;семнадцатьs&quot;</span>;<span style="color: red">&quot;восемнадцатьs&quot;</span>;<span style="color: red">&quot;девятнадцатьs&quot;</span>)
&amp;<span style="color: red">&quot;миллиардов &quot;</span>
;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));12);3)
;2;1)+1
;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;двадцатьs&quot;</span>;<span style="color: red">&quot;тридцатьs&quot;</span>;<span style="color: red">&quot;сорокs&quot;</span>;
<span style="color: red">&quot;пятьдесятs&quot;</span>;<span style="color: red">&quot;шестьдесятs&quot;</span>;<span style="color: red">&quot;семьдесятs&quot;</span>;<span style="color: red">&quot;восемьдесятs&quot;</span>;<span style="color: red">&quot;девяностоs&quot;</span>)
&amp;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));12);3)
)+1
;<span style="color: red">&quot;миллиардов &quot;</span>;<span style="color: red">&quot;одинsмиллиард &quot;</span>;<span style="color: red">&quot;дваsмиллиарда &quot;</span>;<span style="color: red">&quot;триsмиллиарда &quot;</span>;<span style="color: red">&quot;четыреsмиллиарда &quot;</span>;
<span style="color: red">&quot;пятьsмиллиардов &quot;</span>;<span style="color: red">&quot;шестьsмиллиардов &quot;</span>;<span style="color: red">&quot;семьsмиллиардов &quot;</span>;<span style="color: red">&quot;восемьsмиллиардов &quot;</span>;<span style="color: red">&quot;девятьsмиллиардов &quot;</span>)
))
&amp;
(
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));9);3)
+0=0;<span style="color: red">&quot;&quot;</span>
;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));9);3)
)+1
;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;стоs&quot;</span>;<span style="color: red">&quot;двестиs&quot;</span>;<span style="color: red">&quot;тристаs&quot;</span>;<span style="color: red">&quot;четырестаs&quot;</span>;
<span style="color: red">&quot;пятьсотs&quot;</span>;<span style="color: red">&quot;шестьсотs&quot;</span>;<span style="color: red">&quot;семьсотs&quot;</span>;<span style="color: red">&quot;восемьсотs&quot;</span>;<span style="color: red">&quot;девятьсотs&quot;</span>)
&amp;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));9);3)
;2;1)+0=1;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));9);3)
;2)-9
;<span style="color: red">&quot;десятьs&quot;</span>;<span style="color: red">&quot;одиннадцатьs&quot;</span>;<span style="color: red">&quot;двенадцатьs&quot;</span>;<span style="color: red">&quot;тринадцатьs&quot;</span>;<span style="color: red">&quot;четырнадцатьs&quot;</span>;
<span style="color: red">&quot;пятнадцатьs&quot;</span>;<span style="color: red">&quot;шестнадцатьs&quot;</span>;<span style="color: red">&quot;семнадцатьs&quot;</span>;<span style="color: red">&quot;восемнадцатьs&quot;</span>;<span style="color: red">&quot;девятнадцатьs&quot;</span>)
&amp;<span style="color: red">&quot;миллионов &quot;</span>
;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));9);3)
;2;1)+1
;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;двадцатьs&quot;</span>;<span style="color: red">&quot;тридцатьs&quot;</span>;<span style="color: red">&quot;сорокs&quot;</span>;
<span style="color: red">&quot;пятьдесятs&quot;</span>;<span style="color: red">&quot;шестьдесятs&quot;</span>;<span style="color: red">&quot;семьдесятs&quot;</span>;<span style="color: red">&quot;восемьдесятs&quot;</span>;<span style="color: red">&quot;девяностоs&quot;</span>)
&amp;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));9);3)
)+1
;<span style="color: red">&quot;миллионов &quot;</span>;<span style="color: red">&quot;одинsмиллион &quot;</span>;<span style="color: red">&quot;дваsмиллиона &quot;</span>;<span style="color: red">&quot;триsмиллиона &quot;</span>;<span style="color: red">&quot;четыреsмиллиона &quot;</span>;
<span style="color: red">&quot;пятьsмиллионов &quot;</span>;<span style="color: red">&quot;шестьsмиллионов &quot;</span>;<span style="color: red">&quot;семьsмиллионов &quot;</span>;<span style="color: red">&quot;восемьsмиллионов &quot;</span>;<span style="color: red">&quot;девятьsмиллионов &quot;</span>)
))
&amp;
(
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));6);3)
+0=0;<span style="color: red">&quot;&quot;</span>
;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));6);3)
)+1
;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;стоs&quot;</span>;<span style="color: red">&quot;двестиs&quot;</span>;<span style="color: red">&quot;тристаs&quot;</span>;<span style="color: red">&quot;четырестаs&quot;</span>;
<span style="color: red">&quot;пятьсотs&quot;</span>;<span style="color: red">&quot;шестьсотs&quot;</span>;<span style="color: red">&quot;семьсотs&quot;</span>;<span style="color: red">&quot;восемьсотs&quot;</span>;<span style="color: red">&quot;девятьсотs&quot;</span>)
&amp;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));6);3)
;2;1)+0=1;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));6);3)
;2)-9
;<span style="color: red">&quot;десятьs&quot;</span>;<span style="color: red">&quot;одиннадцатьs&quot;</span>;<span style="color: red">&quot;двенадцатьs&quot;</span>;<span style="color: red">&quot;тринадцатьs&quot;</span>;<span style="color: red">&quot;четырнадцатьs&quot;</span>;
<span style="color: red">&quot;пятнадцатьs&quot;</span>;<span style="color: red">&quot;шестнадцатьs&quot;</span>;<span style="color: red">&quot;семнадцатьs&quot;</span>;<span style="color: red">&quot;восемнадцатьs&quot;</span>;<span style="color: red">&quot;девятнадцатьs&quot;</span>)
&amp;<span style="color: red">&quot;тысяч &quot;</span>
;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));6);3)
;2;1)+1
;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;двадцатьs&quot;</span>;<span style="color: red">&quot;тридцатьs&quot;</span>;<span style="color: red">&quot;сорокs&quot;</span>;
<span style="color: red">&quot;пятьдесятs&quot;</span>;<span style="color: red">&quot;шестьдесятs&quot;</span>;<span style="color: red">&quot;семьдесятs&quot;</span>;<span style="color: red">&quot;восемьдесятs&quot;</span>;<span style="color: red">&quot;девяностоs&quot;</span>)
&amp;
((
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));6);3)
)+1
;<span style="color: red">&quot;тысяч &quot;</span>;<span style="color: red">&quot;однаsтысяча &quot;</span>;<span style="color: red">&quot;двеsтысячи &quot;</span>;<span style="color: red">&quot;триsтысячи &quot;</span>;<span style="color: red">&quot;четыреsтысячи &quot;</span>;
<span style="color: red">&quot;пятьsтысяч &quot;</span>;<span style="color: red">&quot;шестьsтысяч &quot;</span>;<span style="color: red">&quot;семьsтысяч &quot;</span>;<span style="color: red">&quot;восемьsтысяч &quot;</span>;<span style="color: red">&quot;девятьsтысяч &quot;</span>)
))
&amp;
(
((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));3)
+0=0;(((ABS(A1);2))=0;<span style="color: red">&quot;нольsрублей&quot;</span>;<span style="color: red">&quot;sрублей&quot;</span>)
;
((
((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));3)
)+1
;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;стоs&quot;</span>;<span style="color: red">&quot;двестиs&quot;</span>;<span style="color: red">&quot;тристаs&quot;</span>;<span style="color: red">&quot;четырестаs&quot;</span>;
<span style="color: red">&quot;пятьсотs&quot;</span>;<span style="color: red">&quot;шестьсотs&quot;</span>;<span style="color: red">&quot;семьсотs&quot;</span>;<span style="color: red">&quot;восемьсотs&quot;</span>;<span style="color: red">&quot;девятьсотs&quot;</span>)
&amp;
((
((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));3)
;2;1)+0=1;
((
((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));3)
;2)-9
;<span style="color: red">&quot;десятьs&quot;</span>;<span style="color: red">&quot;одиннадцатьs&quot;</span>;<span style="color: red">&quot;двенадцатьs&quot;</span>;<span style="color: red">&quot;тринадцатьs&quot;</span>;<span style="color: red">&quot;четырнадцатьs&quot;</span>;
<span style="color: red">&quot;пятнадцатьs&quot;</span>;<span style="color: red">&quot;шестнадцатьs&quot;</span>;<span style="color: red">&quot;семнадцатьs&quot;</span>;<span style="color: red">&quot;восемнадцатьs&quot;</span>;<span style="color: red">&quot;девятнадцатьs&quot;</span>)
&amp;<span style="color: red">&quot;рублей&quot;</span>
;
((
((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));3)
;2;1)+1
;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;двадцатьs&quot;</span>;<span style="color: red">&quot;тридцатьs&quot;</span>;<span style="color: red">&quot;сорокs&quot;</span>;
<span style="color: red">&quot;пятьдесятs&quot;</span>;<span style="color: red">&quot;шестьдесятs&quot;</span>;<span style="color: red">&quot;семьдесятs&quot;</span>;<span style="color: red">&quot;восемьдесятs&quot;</span>;<span style="color: red">&quot;девяностоs&quot;</span>)
&amp;
((
((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));3)
)+1
;<span style="color: red">&quot;&quot;</span>;(1;<span style="color: red">&quot;одинs&quot;</span>;<span style="color: red">&quot;однаs&quot;</span>;<span style="color: red">&quot;одноs&quot;</span>);
(1;<span style="color: red">&quot;дваs&quot;</span>;<span style="color: red">&quot;двеs&quot;</span>;<span style="color: red">&quot;дваs&quot;</span>);<span style="color: red">&quot;триs&quot;</span>;<span style="color: red">&quot;четыреs&quot;</span>;
<span style="color: red">&quot;пятьs&quot;</span>;<span style="color: red">&quot;шестьs&quot;</span>;<span style="color: red">&quot;семьs&quot;</span>;<span style="color: red">&quot;восемьs&quot;</span>;<span style="color: red">&quot;девятьs&quot;</span>)
&amp;
(((
((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));3)
)+1;3;1;2;2;2;3;3;3;3;3);
<span style="color: red">&quot;рубль&quot;</span>;<span style="color: red">&quot;рубля&quot;</span>;<span style="color: red">&quot;рублей&quot;</span>)
))
);<span style="color: red">&quot;s&quot;</span>;<span style="color: red">&quot; &quot;</span>);<span style="color: red">&quot;S&quot;</span>;<span style="color: red">&quot;&quot;</span>)
&amp;
<span style="color: red">&quot; &quot;</span> &amp; (
((<span style="color: red">&quot;0&quot;</span>;((2/3)+1)*3)&amp;(((ABS(A1);2)-((ABS(A1);2)))*10^2;0);((2/3)+1)*3)
;2)&amp;<span style="color: red">&quot; &quot;</span>
&amp;
(((
((<span style="color: red">&quot;0&quot;</span>;((2/3)+1)*3)&amp;(((ABS(A1);2)-((ABS(A1);2)))*10^2;0);((2/3)+1)*3)
;3)+0=0;((
((<span style="color: red">&quot;0&quot;</span>;((2/3)+1)*3)&amp;(((ABS(A1);2)-((ABS(A1);2)))*10^2;0);((2/3)+1)*3)
;3);2;1)+0=1);
<span style="color: red">&quot;копеек&quot;</span>;
((((
((<span style="color: red">&quot;0&quot;</span>;((2/3)+1)*3)&amp;(((ABS(A1);2)-((ABS(A1);2)))*10^2;0);((2/3)+1)*3)
;3))+1;3;1;2;2;2;3;3;3;3;3);
<span style="color: red">&quot;копейка&quot;</span>;<span style="color: red">&quot;копейки&quot;</span>;<span style="color: red">&quot;копеек&quot;</span>)
)</pre></div>В отличие от вышеприведенных метода и функции, &quot;проговаривающих&quot; только целые числа, эта формула содержит еще и блок копеек, т.е. дробной части. Формула также приводится в прилагаемом файле, чтобы владельцы копий Excel, отличных от русской, либо русской, но с иными разделителями, тоже могли ею воспользоваться. <br />
<br />
Буковка &quot;s&quot; на конце числительных символизирует &quot;пробел&quot; (&quot;space&quot;) и предназначена для корректной работы функции ПРОПНАЧ по переводу в верхний регистр только первой буквы каждой триады (иначе, в случае настоящего пробела, прописной стала бы первая буква в каждом слове).<br />
<br />
Верхняя граница действия формулы - 999 триллионов (15-тизначное число). Если такие гигантские суммы не предполагается прописывать словами в повседневной хозяйственной практике, то формулу можно подрезать, удалив из нее, скажем, блоки триллионов и миллиардов, ограничив, таким образом, ее значением суммы в 999 миллионов, а то и 999 тысяч, если удалить еще и блок миллионов.<br />
<br />
Для облегчения ориентирования в тексте формулы - блок триллионов ограничен фрагментами (фрагменты входят в состав блока):<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">(
(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));15);3)
+0=0;<span style="color: red">&quot;&quot;</span>
;
.......................................................................
.......................................................................
.......................................................................
;<span style="color: red">&quot;триллионов &quot;</span>;<span style="color: red">&quot;одинsтриллион &quot;</span>;<span style="color: red">&quot;дваsтриллиона &quot;</span>;<span style="color: red">&quot;триsтриллиона &quot;</span>;<span style="color: red">&quot;четыреsтриллиона &quot;</span>;
<span style="color: red">&quot;пятьsтриллионов &quot;</span>;<span style="color: red">&quot;шестьsтриллионов &quot;</span>;<span style="color: red">&quot;семьsтриллионов &quot;</span>;<span style="color: red">&quot;восемьsтриллионов &quot;</span>;<span style="color: red">&quot;девятьsтриллионов &quot;</span>)
))
&amp;</pre></div>Блоки остальных разрядностей можно найти по аналогии. Дополнительным средством идентификации блока триллионов может быть второе число 15 в характерной подстроке ЛЕВСИМВ(ПРАВСИМВ(ПОВТОР(&quot;0&quot;;15)&amp;ЦЕЛОЕ(ОКРУГЛ(ABS(A1);2));15);3). Для миллиардов это число будет 12, для миллионов - 9, для тысяч - 6 (думаю, комментарии излишни).<br />
<br />
Конечно же, с вычислительной точки зрения мегаформула (назовем ее &quot;алгоритмом в одной ячейке&quot;) вопиюще неоптимальна. Невооруженным глазом видны многократно повторяющиеся одинаковые фрагменты. И всё в угоду тому, чтобы содержать ссылки на одну единственную ячейку A1 - и тем самым производить впечатление экспоната кунсткамеры. Но, согласитесь, прикольно! А современные компы потянут и не такие вычисления ;)<br />
<br />
Если же количество задействованных для расчета суммы прописью ячеек для нас не имеет значения (в разумных пределах), то можно использовать &quot;алгоритм в нескольких ячейках&quot;. Общее количество символов во всех используемых при этом формулах будет существенно меньше и в самой &quot;насыщенной&quot; не превысит значения 1024, что позволит применить &quot;алгоритм в нескольких ячейках&quot; также и в более ранних версиях Excel (до 2007).<br />
<br />
Ниже введем на рабочем листе Excel несколько формул, являющихся составными частями алгоритма в нескольких ячейках. Общее количество задействованных ячеек - 12. Можно уменьшить до 10, если объединить в одной ячейке &quot;рубли&quot;, &quot;копейки&quot; и &quot;сцепить&quot; (если только не актуально ограничение на длину формулы в 1024 символа). И дальше - уменьшение на 2 ячейки при каждом отказе от использования старших разрядов (триллионов, миллиардов и т.д.). Для сумм меньше миллиона можно будет уложиться в 4 ячейки.<br />
<br />
Итак, формула для ячеек B1:F1 - в них будут отображаться триады цифрами (диапазон содержит 5 ячеек):<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">=(((<span style="color: red">&quot;0&quot;</span>;15)&amp;((ABS(A1);2));{15;12;9;6;3});3)</pre></div>Эта формула массива должна быть введена при помощи следующих шагов: копируем ее текст отсюда; в Excel выделяем диапазон B1:F1; жмем F2 для перехода в режим редактирования; в строке редактирования делаем вставку из буфера; завершаем ввод нажатием комбинации Shift+Ctrl+Enter.<br />
<br />
Для ячеек G1:J1 - триады словами (4 ячейки!):<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">=((
(B1:E1+0=0
;
<span style="color: red">&quot;&quot;</span>
;
((B1:E1)+1;
<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;стоs&quot;</span>;<span style="color: red">&quot;двестиs&quot;</span>;<span style="color: red">&quot;тристаs&quot;</span>;<span style="color: red">&quot;четырестаs&quot;</span>;
<span style="color: red">&quot;пятьсотs&quot;</span>;<span style="color: red">&quot;шестьсотs&quot;</span>;<span style="color: red">&quot;семьсотs&quot;</span>;<span style="color: red">&quot;восемьсотs&quot;</span>;<span style="color: red">&quot;девятьсотs&quot;</span>)
&amp;
((B1:E1;2;1)+0=1
;
((B1:E1;2)-9;
<span style="color: red">&quot;десятьs&quot;</span>;<span style="color: red">&quot;одиннадцатьs&quot;</span>;<span style="color: red">&quot;двенадцатьs&quot;</span>;<span style="color: red">&quot;тринадцатьs&quot;</span>;<span style="color: red">&quot;четырнадцатьs&quot;</span>;
<span style="color: red">&quot;пятнадцатьs&quot;</span>;<span style="color: red">&quot;шестнадцатьs&quot;</span>;<span style="color: red">&quot;семнадцатьs&quot;</span>;<span style="color: red">&quot;восемнадцатьs&quot;</span>;<span style="color: red">&quot;девятнадцатьs&quot;</span>)
&amp;{<span style="color: red">&quot;триллионов &quot;</span>;<span style="color: red">&quot;миллиардов &quot;</span>;<span style="color: red">&quot;миллионов &quot;</span>;<span style="color: red">&quot;тысяч &quot;</span>}
;
((B1:E1;2;1)+1;
<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;двадцатьs&quot;</span>;<span style="color: red">&quot;тридцатьs&quot;</span>;<span style="color: red">&quot;сорокs&quot;</span>;
<span style="color: red">&quot;пятьдесятs&quot;</span>;<span style="color: red">&quot;шестьдесятs&quot;</span>;<span style="color: red">&quot;семьдесятs&quot;</span>;<span style="color: red">&quot;восемьдесятs&quot;</span>;<span style="color: red">&quot;девяностоs&quot;</span>)
&amp;
((B1:E1)+1;
<span style="color: red">&quot;&quot;</span>;({1;1;1;1};<span style="color: red">&quot;одинs&quot;</span>;<span style="color: red">&quot;однаs&quot;</span>;<span style="color: red">&quot;одноs&quot;</span>);
({1;1;1;2};<span style="color: red">&quot;дваs&quot;</span>;<span style="color: red">&quot;двеs&quot;</span>;<span style="color: red">&quot;дваs&quot;</span>);<span style="color: red">&quot;триs&quot;</span>;<span style="color: red">&quot;четыреs&quot;</span>;
<span style="color: red">&quot;пятьs&quot;</span>;<span style="color: red">&quot;шестьs&quot;</span>;<span style="color: red">&quot;семьs&quot;</span>;<span style="color: red">&quot;восемьs&quot;</span>;<span style="color: red">&quot;девятьs&quot;</span>)
&amp;
{<span style="color: red">&quot;триллион&quot;</span>;<span style="color: red">&quot;миллиард&quot;</span>;<span style="color: red">&quot;миллион&quot;</span>;<span style="color: red">&quot;тысяч&quot;</span>}&amp;
(((B1:E1)+1;3;1;2;2;2;3;3;3;3;3);
{<span style="color: red">&quot; &quot;</span>;<span style="color: red">&quot; &quot;</span>;<span style="color: red">&quot; &quot;</span>;<span style="color: red">&quot;а &quot;</span>};{<span style="color: red">&quot;а &quot;</span>;<span style="color: red">&quot;а &quot;</span>;<span style="color: red">&quot;а &quot;</span>;<span style="color: red">&quot;и &quot;</span>};{<span style="color: red">&quot;ов &quot;</span>;<span style="color: red">&quot;ов &quot;</span>;<span style="color: red">&quot;ов &quot;</span>;<span style="color: red">&quot; &quot;</span>})
))
);<span style="color: red">&quot;s&quot;</span>;<span style="color: red">&quot; &quot;</span>)</pre></div>Эта тоже формула массива, которая вводится тем же способом, что и предыдущая. Следует обратить внимание, что эта формула будет занимать 4 ячейки, а не 5 как предыдущая. 5-ю ячейку (рублей) мы введем отдельно (почему - будет понятно далее из файла, при рассмотрении параметрического варианта алгоритма).<br />
<br />
Для ячейки K1 - рубли (обычная формула - ввод завершается простым нажатием Enter):<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">=(((
(F1+0=0
;
((ABS(A1))=0;<span style="color: red">&quot;нольsрублей&quot;</span>;<span style="color: red">&quot;sрублей&quot;</span>)
;
((F1)+1;
<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;стоs&quot;</span>;<span style="color: red">&quot;двестиs&quot;</span>;<span style="color: red">&quot;тристаs&quot;</span>;<span style="color: red">&quot;четырестаs&quot;</span>;
<span style="color: red">&quot;пятьсотs&quot;</span>;<span style="color: red">&quot;шестьсотs&quot;</span>;<span style="color: red">&quot;семьсотs&quot;</span>;<span style="color: red">&quot;восемьсотs&quot;</span>;<span style="color: red">&quot;девятьсотs&quot;</span>)
&amp;
((F1;2;1)+0=1
;
((F1;2)-9;
<span style="color: red">&quot;десятьs&quot;</span>;<span style="color: red">&quot;одиннадцатьs&quot;</span>;<span style="color: red">&quot;двенадцатьs&quot;</span>;<span style="color: red">&quot;тринадцатьs&quot;</span>;<span style="color: red">&quot;четырнадцатьs&quot;</span>;
<span style="color: red">&quot;пятнадцатьs&quot;</span>;<span style="color: red">&quot;шестнадцатьs&quot;</span>;<span style="color: red">&quot;семнадцатьs&quot;</span>;<span style="color: red">&quot;восемнадцатьs&quot;</span>;<span style="color: red">&quot;девятнадцатьs&quot;</span>)
&amp;<span style="color: red">&quot;рублей&quot;</span>
;
((F1;2;1)+1;
<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;&quot;</span>;<span style="color: red">&quot;двадцатьs&quot;</span>;<span style="color: red">&quot;тридцатьs&quot;</span>;<span style="color: red">&quot;сорокs&quot;</span>;
<span style="color: red">&quot;пятьдесятs&quot;</span>;<span style="color: red">&quot;шестьдесятs&quot;</span>;<span style="color: red">&quot;семьдесятs&quot;</span>;<span style="color: red">&quot;восемьдесятs&quot;</span>;<span style="color: red">&quot;девяностоs&quot;</span>)
&amp;
((F1)+1;
<span style="color: red">&quot;&quot;</span>;(1;<span style="color: red">&quot;одинs&quot;</span>;<span style="color: red">&quot;однаs&quot;</span>;<span style="color: red">&quot;одноs&quot;</span>);
(1;<span style="color: red">&quot;дваs&quot;</span>;<span style="color: red">&quot;двеs&quot;</span>;<span style="color: red">&quot;дваs&quot;</span>);<span style="color: red">&quot;триs&quot;</span>;<span style="color: red">&quot;четыреs&quot;</span>;
<span style="color: red">&quot;пятьs&quot;</span>;<span style="color: red">&quot;шестьs&quot;</span>;<span style="color: red">&quot;семьs&quot;</span>;<span style="color: red">&quot;восемьs&quot;</span>;<span style="color: red">&quot;девятьs&quot;</span>)
&amp;
(((F1)+1;3;1;2;2;2;3;3;3;3;3);
<span style="color: red">&quot;рубль&quot;</span>;<span style="color: red">&quot;рубля&quot;</span>;<span style="color: red">&quot;рублей&quot;</span>)
))
);<span style="color: red">&quot;s&quot;</span>;<span style="color: red">&quot; &quot;</span>);<span style="color: red">&quot;S&quot;</span>;<span style="color: red">&quot;&quot;</span>)</pre></div><br />
Для ячейки L1 - копейки (обычная формула - ввод завершается простым нажатием Enter):<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">=<span style="color: red">&quot; &quot;</span> &amp; (
((<span style="color: red">&quot;0&quot;</span>;((2/3)+1)*3)&amp;(((ABS(A1);2)-((ABS(A1);2)))*10^2;0);((2/3)+1)*3)
;2)&amp;<span style="color: red">&quot; &quot;</span>
&amp;
(((
((<span style="color: red">&quot;0&quot;</span>;((2/3)+1)*3)&amp;(((ABS(A1);2)-((ABS(A1);2)))*10^2;0);((2/3)+1)*3)
;3)+0=0;((
((<span style="color: red">&quot;0&quot;</span>;((2/3)+1)*3)&amp;(((ABS(A1);2)-((ABS(A1);2)))*10^2;0);((2/3)+1)*3)
;3);2;1)+0=1);
<span style="color: red">&quot;копеек&quot;</span>;
((((
((<span style="color: red">&quot;0&quot;</span>;((2/3)+1)*3)&amp;(((ABS(A1);2)-((ABS(A1);2)))*10^2;0);((2/3)+1)*3)
;3))+1;3;1;2;2;2;3;3;3;3;3);
<span style="color: red">&quot;копейка&quot;</span>;<span style="color: red">&quot;копейки&quot;</span>;<span style="color: red">&quot;копеек&quot;</span>)
)</pre></div>Возможно, вы заметили, что в этой формуле (и в других) присутствуют фрагменты, которые могли бы быть вычислены заранее и введены в формулу как константы, например: (ЦЕЛОЕ(2/3)+1)*3) или 10^2. Это сделано сознательно - в таком виде легче конструировать универсальные параметрические формулы на случаи любых единиц измерения, а не только рублей и копеек. В параметрическом варианте (см. в файле) число 2 в этих примерах, которое по сути представляет собой кол-во знаков дробной части, заменено ссылкой на ячейку, в которой хранится данная величина.<br />
<br />
Наконец, в ячейке M1 получаем окончательную сумму прописью:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">=(G1;H1;I1;J1;K1;L1)

<span style="color: green">// удивительно упорство Microsoft по непредоставлению до сих пор возможности записи этой формулы в виде =СЦЕПИТЬ(G1:L1)</span></pre></div>Простую формулу в ячейке M1 можно усложнить дополнительной обработкой по желанию, например, оставив заглавной только самую первую букву фразы:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">=((G1;H1;I1;J1;K1;L1))&amp;(((G1;H1;I1;J1;K1;L1);2;1000))

<span style="color: green">// здесь 1000 - произвольное, заведомо большее длины строки число</span></pre></div>В прилагаемом файле на основе приведенных формул алгоритма в нескольких ячейках демонстрируется также его параметрический вариант - на случай любых единиц измерения. Настройка на любые единицы выполняется путем указания необходимых параметров (род единиц, формы считаемых существительных, кол-во знаков дробной части) в дополнительных ячейках. С несложными деталями можно ознакомиться самостоятельно. Файл создан в Excel 2010.</div>


<!-- attachments -->
	<div style="margin-top:10px">

		
		
		
		
			<fieldset class="fieldset">
				<legend>Вложения</legend>
				<table cellpadding="0" cellspacing="3" border="0">
				<tr>
	<td><img class="inlineimg" src="http://axforum.info//img.axforum.info/attach/xlsx.gif" alt="Тип файла: xlsx" width="16" height="16" border="0" style="vertical-align:baseline" /></td>
	<td><a href="//axforum.info/forums/blog_attachment.php?attachmentid=210&amp;d=1331637405">SumInWords_Excel2010.xlsx</a> (18.3 Кб, 3916 просмотров)</td>
</tr>
				</table>
			</fieldset>
		

	</div>
<!-- / attachments -->
]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=112</guid>
		</item>
		<item>
			<title>Свойство Command кнопки CommandButton</title>
			<link>//axforum.info/forums/blog.php?b=134</link>
			<pubDate>Tue, 01 Jun 2010 15:05:59 GMT</pubDate>
			<description><![CDATA[Возможно, я что-то пропустил на своем аксаптовском пути или что-то не так понял, но когда мне захотелось на форме динамически создать командную кнопку (FormCommandButtonControl) и нагрузить ее предопределенной командой - OK или Cancel, я не смог найти в своих подручных источниках информации вразумительного списка команд, пригодного для использования при разработке. Конечно, в поле свойства Command выпадающий список появляется, но получить его оттуда полностью как-то не получается. Я полагал, что, как водится в подобных случаях, существует какое-нибудь перечисление (enum) и команда легко задается константой вроде Command::Ok. 

Ничего подобного не обнаружилось (или я плохо искал). Поэтому пришлось создавать тестовую форму, помещать на нее единственный контрол - командную кнопку, устанавливать свойство Command этой кнопки в OK и в метод init формы вставлять оператор, информирующий после запуска формы о целочисленном значении, соответствующем этой команде:

public void init()
{
    FormCommandButtonControl    button;

    super();

    button = element.design().controlNum(1);
    box::info(strFmt('Код команды: %1', button.command())); // 263 - ОК
}
Разумеется, мне тут же захотелось по аналогии получить список всех команд. Усилия привели к следующему джобу, на ходу создающему форму, кнопку и выводящему желаемую информацию в инфолог (форма за всё это время даже не появляется на экране) :

static void job_getCommandButtonCommandList(Args _args)
{
    Args                        args    = new Args();
    Form                        form    = new Form();
    FormRun                     formRun;
    FormCommandButtonControl    commandButton;
    int                         i;
    str                         currCaption,prevCaption;
;
    form.addDesign('Design');
    args.object(form);

    formRun = classFactory.formRunClass(args);
    formRun.init();

    commandButton = formRun.design().addControl(FormControlType::CommandButton, 'CommandButton');

    for (i=0;i<=10000;i++)
    {
        commandButton.command(i);
        currCaption = CommandButton.caption();

        if (currCaption != prevCaption)
            info(strFmt('%1 -- %2 -- %3', i, commandButton.caption(), commandButton.toolTip()));

        prevCaption = currCaption;
    }

    formRun.close();
}
В результате выполнения джоба вы становитесь счастливым обладателем актуальной информации о паре сотен команд: числовой код, название (подпись на кнопке) и текст всплывающей подсказки (последний особенно помогает в случае повторяющихся названий команд - некоторое количество таковых присутствует в списке, причем, в разных его местах). 

Верхний предел цикла - 10000 - был подобран эмпирически, с целью заведомого покрытия диапазона имеющихся значений кодов команд от 257 - "Справка" до 4388 - "Создать из файла" (для Ax 3.0 SP4; в версии 2009 список команд побольше). Было также обнаружено, что если к обеим границам цикла прибавить 65536*N (где N = 1..65536), то последовательность кодов повторяется в других числовых диапазонах и с другими абсолютными значениями. Это говорит о том, что в свойстве command система использует лишь 4 младших байта передаваемого туда целого числа.

P.S. В процессе тестирования джоба в Ax 3.0 SP4 я сверил его результаты с содержимым выпадающего списка свойства Command. Для этого сравнения мне пришлось фрагментарно наскриншотить список. Публикую его здесь на память, чтобы труд не пропал даром (порядок следования частей определяется числами сверху):

Изображение: http://www.axforum.info/forums/picture.php?albumid=9&pictureid=15 

Джоб вывел в инфолог 199 команд (Ax 3.0 SP4). В списке же их - 205. Абсолютно повторяются (caption и tooltip): "Сохранить" - 2 раза, "Точки останова" - 2 раза, "Удаление всех точек останова" - 3 раза, "Вставка/удаление точек останова" - 2 раза, "Блокировка/разблокировка точек останова" - 2 раза. Итого имеем "лишних": 1+1+2+1+1 = 6 строк = 205 - 199.

P.S. 10.08.2010. P.S. Обнаружена идентичность параметров n (кодов команд), передаваемых методам FormCommandButtonControl.command(n) и FormRun.task(n): Как программно снять "фильтр по выделению" (http://www.axforum.info/forums/showthread.php?p=229827#post229827)]]></description>
			<content:encoded><![CDATA[<div>Возможно, я что-то пропустил на своем аксаптовском пути или что-то не так понял, но когда мне захотелось на форме динамически создать командную кнопку (FormCommandButtonControl) и нагрузить ее предопределенной командой - OK или Cancel, я не смог найти в своих подручных источниках информации вразумительного списка команд, пригодного для использования при разработке. Конечно, в поле свойства Command выпадающий список появляется, но получить его оттуда полностью как-то не получается. Я полагал, что, как водится в подобных случаях, существует какое-нибудь перечисление (enum) и команда легко задается константой вроде Command::Ok. <br />
<br />
Ничего подобного не обнаружилось (или я плохо искал). Поэтому пришлось создавать тестовую форму, помещать на нее единственный контрол - командную кнопку, устанавливать свойство Command этой кнопки в OK и в метод init формы вставлять оператор, информирующий после запуска формы о целочисленном значении, соответствующем этой команде:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">public</span> <span style="color: blue">void</span> init()
{
    FormCommandButtonControl    button;

    <span style="color: blue">super</span>();

    button = element.design().controlNum(1);
    box::info(strFmt(<span style="color: red">'Код команды: %1'</span>, button.command())); <span style="color: green">// 263 - ОК
</span>}</pre></div>Разумеется, мне тут же захотелось по аналогии получить список всех команд. Усилия привели к следующему джобу, на ходу создающему форму, кнопку и выводящему желаемую информацию в инфолог (форма за всё это время даже не появляется на экране) :<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">static</span> <span style="color: blue">void</span> job_getCommandButtonCommandList(Args _args)
{
    Args                        args    = <span style="color: blue">new</span> Args();
    Form                        form    = <span style="color: blue">new</span> Form();
    FormRun                     formRun;
    FormCommandButtonControl    commandButton;
    <span style="color: blue">int</span>                         i;
    <span style="color: blue">str</span>                         currCaption,prevCaption;
;
    form.addDesign(<span style="color: red">'Design'</span>);
    args.object(form);

    formRun = classFactory.formRunClass(args);
    formRun.init();

    commandButton = formRun.design().addControl(FormControlType::CommandButton, <span style="color: red">'CommandButton'</span>);

    <span style="color: blue">for</span> (i=0;i&lt;=10000;i++)
    {
        commandButton.command(i);
        currCaption = CommandButton.caption();

        <span style="color: blue">if</span> (currCaption != prevCaption)
            info(strFmt(<span style="color: red">'%1 -- %2 -- %3'</span>, i, commandButton.caption(), commandButton.toolTip()));

        prevCaption = currCaption;
    }

    formRun.close();
}</pre></div>В результате выполнения джоба вы становитесь счастливым обладателем актуальной информации о паре сотен команд: числовой код, название (подпись на кнопке) и текст всплывающей подсказки (последний особенно помогает в случае повторяющихся названий команд - некоторое количество таковых присутствует в списке, причем, в разных его местах). <br />
<br />
Верхний предел цикла - 10000 - был подобран эмпирически, с целью заведомого покрытия диапазона имеющихся значений кодов команд от 257 - &quot;Справка&quot; до 4388 - &quot;Создать из файла&quot; (для Ax 3.0 SP4; в версии 2009 список команд побольше). Было также обнаружено, что если к обеим границам цикла прибавить 65536*N (где N = 1..65536), то последовательность кодов повторяется в других числовых диапазонах и с другими абсолютными значениями. Это говорит о том, что в свойстве command система использует лишь 4 младших байта передаваемого туда целого числа.<br />
<br />
P.S. В процессе тестирования джоба в Ax 3.0 SP4 я сверил его результаты с содержимым выпадающего списка свойства Command. Для этого сравнения мне пришлось фрагментарно наскриншотить список. Публикую его здесь на память, чтобы труд не пропал даром (порядок следования частей определяется числами сверху):<br />
<br />
<img src="http://www.axforum.info/forums/picture.php?albumid=9&amp;pictureid=15" border="0" alt="" /><br />
<br />
Джоб вывел в инфолог 199 команд (Ax 3.0 SP4). В списке же их - 205. Абсолютно повторяются (caption и tooltip): &quot;Сохранить&quot; - 2 раза, &quot;Точки останова&quot; - 2 раза, &quot;Удаление всех точек останова&quot; - 3 раза, &quot;Вставка/удаление точек останова&quot; - 2 раза, &quot;Блокировка/разблокировка точек останова&quot; - 2 раза. Итого имеем &quot;лишних&quot;: 1+1+2+1+1 = 6 строк = 205 - 199.<br />
<br />
P.S. 10.08.2010. P.S. Обнаружена идентичность параметров n (кодов команд), передаваемых методам FormCommandButtonControl.command(n) и FormRun.task(n): <a href="http://www.axforum.info/forums/showthread.php?p=229827#post229827" target="_blank">Как программно снять &quot;фильтр по выделению&quot;</a></div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=134</guid>
		</item>
		<item>
			<title><![CDATA[Круговорот значений в lookup'е]]></title>
			<link>//axforum.info/forums/blog.php?b=115</link>
			<pubDate>Wed, 19 May 2010 14:41:41 GMT</pubDate>
			<description><![CDATA[Как известно, в Аксапте распространены два типа управляющих элементов (контролов) с выпадающим списком. Один из них - ComboBox, служащий для выбора и отображения списка значений конкретного перечисления (enum'а). Другой - редактируемое поле, обычно текстовое (StringEdit), значения для которого выбираются из специальной lookup-формы, отображающей строки определенной таблицы. Механизм отображения lookup-формы работает так, что строки для выбора появляются сразу под редактируемым полем, во много напоминая поведение ComboBox. 

Помимо общего "выпадающего" характера оба контрола совпадают и в способе раскрытия списка значений при помощи клавиатуры: Alt-СтрелкаВниз. А вот от еще одной полезной особенности ComboBox'а поле с lookup'ом невыгодно (пока!) отличается. Речь идёт о возможности перебора значений ComboBox'а при помощи двойного щелчка мышкой по этому полю. При каждом очередном щелчке в поле подставляется следующее по очереди значение перечисления, без необходимости раскрывать список. По достижении последнего значения очередь опять переходит к первому, второму и т.д. по кругу. Если при выполнении щелчков удерживать нажатой клавишу Shift, то перебор элементов осуществляется в обратном направлении, также по кругу.

Подобное поведение можно имитировать в форме и на контроле с lookup'ом. Для этого придется написать несложную обработку события mouseDblClick. В качестве примера приведу текст моего метода, предназначенного в нашей Аксапте от GMCS для изменения значение специфического поля "Первичная группа пользователей" (UserGroupDim в таблице SysUserInfo) в форме "Параметры" (SysUserSetup): 

public int mouseDblClick(int _x, int _y, int _button, boolean _Ctrl, boolean _Shift)
{
    int                 ret;
    str                 groupToFind;
    container           filials = ['ГЛАВК','ФилМ','ФилК','ФилП','ФилВ','ФилА',''];
    int                 pos;

    ret = super(_x, _y, _button, _Ctrl, _Shift);

    groupToFind = SysUserInfo.UserGroupDim;

    pos = conFind(filials, groupToFind); // позиция текущего отображаемого элемента

    if (_Shift) // если нажат Shift, то перебор в обратном направлении
    {
        pos--;
        if (pos < 1) pos = conLen(filials);
    }
    else // иначе - в прямом
    {
        pos++;
        if (pos > conLen(filials)) pos = 1;
    }

    SysUserInfo.UserGroupDim = conPeek(filials, pos);
    SysUserInfo.write();

    SysUserInfo_ds.reread();
    SysUserInfo_ds.refresh();

    return ret;
}
Список нужных для перебора элементов (филиалов) у меня жёстко прописан в контейнере. Это потому, что изменяется он крайне редко - за год я добавил в него один элемент (и мне не сложно это сделать, исправив код, а накручивание здесь какого-то более "правильного" функционала - экономически не оправдано). Но при необходимости можно организовать чтение этого списка и из таблицы:

    while select таблица order by ... where ...
    {
        filials += таблица.НужноеПоле;
    }
Особое внимание обращаю на то, что список перебора для большего удобства можно сознательно ограничить. У нас, например, количество строк для выбора в lookup-форме, выпадающей из поля "Первичная группа пользователей", на порядок больше, нежели количество элементов контейнера. Но нам так надо! ;)]]></description>
			<content:encoded><![CDATA[<div>Как известно, в Аксапте распространены два типа управляющих элементов (контролов) с выпадающим списком. Один из них - ComboBox, служащий для выбора и отображения списка значений конкретного перечисления (enum'а). Другой - редактируемое поле, обычно текстовое (StringEdit), значения для которого выбираются из специальной lookup-формы, отображающей строки определенной таблицы. Механизм отображения lookup-формы работает так, что строки для выбора появляются сразу под редактируемым полем, во много напоминая поведение ComboBox. <br />
<br />
Помимо общего &quot;выпадающего&quot; характера оба контрола совпадают и в способе раскрытия списка значений при помощи клавиатуры: Alt-СтрелкаВниз. А вот от еще одной полезной особенности ComboBox'а поле с lookup'ом невыгодно (пока!) отличается. Речь идёт о возможности перебора значений ComboBox'а при помощи двойного щелчка мышкой по этому полю. При каждом очередном щелчке в поле подставляется следующее по очереди значение перечисления, без необходимости раскрывать список. По достижении последнего значения очередь опять переходит к первому, второму и т.д. по кругу. Если при выполнении щелчков удерживать нажатой клавишу Shift, то перебор элементов осуществляется в обратном направлении, также по кругу.<br />
<br />
Подобное поведение можно имитировать в форме и на контроле с lookup'ом. Для этого придется написать несложную обработку события mouseDblClick. В качестве примера приведу текст моего метода, предназначенного в нашей Аксапте от GMCS для изменения значение специфического поля &quot;Первичная группа пользователей&quot; (UserGroupDim в таблице SysUserInfo) в форме &quot;Параметры&quot; (SysUserSetup): <br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">public</span> <span style="color: blue">int</span> mouseDblClick(<span style="color: blue">int</span> _x, <span style="color: blue">int</span> _y, <span style="color: blue">int</span> _button, boolean _Ctrl, boolean _Shift)
{
    <span style="color: blue">int</span>                 ret;
    <span style="color: blue">str</span>                 groupToFind;
    <span style="color: blue">container</span>           filials = [<span style="color: red">'ГЛАВК'</span>,<span style="color: red">'ФилМ'</span>,<span style="color: red">'ФилК'</span>,<span style="color: red">'ФилП'</span>,<span style="color: red">'ФилВ'</span>,<span style="color: red">'ФилА'</span>,<span style="color: red">''</span>];
    <span style="color: blue">int</span>                 pos;

    ret = <span style="color: blue">super</span>(_x, _y, _button, _Ctrl, _Shift);

    groupToFind = SysUserInfo.UserGroupDim;

    pos = conFind(filials, groupToFind); <span style="color: green">// позиция текущего отображаемого элемента
</span>
    <span style="color: blue">if</span> (_Shift) <span style="color: green">// если нажат Shift, то перебор в обратном направлении
</span>    {
        pos--;
        <span style="color: blue">if</span> (pos &lt; 1) pos = conLen(filials);
    }
    <span style="color: blue">else</span> <span style="color: green">// иначе - в прямом
</span>    {
        pos++;
        <span style="color: blue">if</span> (pos &gt; conLen(filials)) pos = 1;
    }

    SysUserInfo.UserGroupDim = conPeek(filials, pos);
    SysUserInfo.write();

    SysUserInfo_ds.reread();
    SysUserInfo_ds.refresh();

    <span style="color: blue">return</span> ret;
}</pre></div>Список нужных для перебора элементов (филиалов) у меня жёстко прописан в контейнере. Это потому, что изменяется он крайне редко - за год я добавил в него один элемент (и мне не сложно это сделать, исправив код, а накручивание здесь какого-то более &quot;правильного&quot; функционала - экономически не оправдано). Но при необходимости можно организовать чтение этого списка и из таблицы:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">    <span style="color: blue">while</span> <span style="color: blue">select</span>  <span style="color: blue">order</span> <span style="color: blue">by</span> ... <span style="color: blue">where</span> ...
    {
        filials += .;
    }</pre></div>Особое внимание обращаю на то, что список перебора для большего удобства можно сознательно ограничить. У нас, например, количество строк для выбора в lookup-форме, выпадающей из поля &quot;Первичная группа пользователей&quot;, на порядок больше, нежели количество элементов контейнера. Но нам так надо! ;)</div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=115</guid>
		</item>
		<item>
			<title>Построение сводной таблицы на форме с загрузкой данных из ADODB.Recordset</title>
			<link>//axforum.info/forums/blog.php?b=120</link>
			<pubDate>Mon, 17 May 2010 15:00:33 GMT</pubDate>
			<description><![CDATA[Сразу определимся с терминами. "Сводная таблица на форме" - это Office Web Component PivotTable, размещенный как элемент управления ActiveX на форме Аксапты. "ADODB.Recordset" - в первую (и наиболее интересную для нас) очередь отсоединенный (или неприсоединенный, или, если угодно, disconnected), в общем, не связанный с базой данных набор записей, формируемый программистом в оперативной памяти (процесс во многом напоминает заполнение массива: Вывод в Excel через Array (http://www.axforum.info/forums/showpost.php?p=224083)). Работа с отсоединенным ADODB.Recordset подробно описана в теме Поговорим об ADO (http://www.axforum.info/forums/showthread.php?t=12973), а также в ряде других смежных обсуждений (Строка в Excel (http://axforum.info/forums/showthread.php?t=27234), экспорт в шаблон excel (http://axforum.info/forums/showthread.php?t=16714) и т.п.).

До недавнего времени я не знал о существовании возможности подачи данных в OWC PivotTable из отсоединенного Recordset'а, будучи, тем не менее, в курсе подобной возможности для сводной таблицы в Excel через PivoteCache (Вывод в Excel сводной таблицы "пользователи-группы" (http://www.axforum.info/forums/blog.php?b=60)).  В том своем сообщении я очень сожалел об отсутствии объекта PivotCache у OWC PivotTable. И думал, что данные могут поступать только из Recordset'ов, связанных с реальными таблицами БД. 

Оказалась, что возможность загрузки из неприсоединенного Recordset'а всё-таки существует! Не в том смысле, что нашёлся PivotCache у PivotTable - его как не было, так и нет - и это на сегодняшний день медицинский факт. Но в том смысле, что загружать данные всё-таки можно, хотя и несколько идеологически иначе - при помощи метода PivotTable.DataSource( recordset ): см. Сводные таблицы и Olap в Dax2009 (http://axforum.info/forums/showthread.php?t=33020).

Приводимый ниже джоб - мой первый опыт работы с OWC PivoteTable на форме. Поэтому можно говорить о том, что публикую я его больше как узелок на память для самого себя, нежели как "лекцию" по пивотостроению (хм, и здесь пиво... :) ).

Джоб решает по сути абсолютно ту же задачу, что и джоб из сообщения Вывод в Excel сводной таблицы "пользователи-группы" (http://www.axforum.info/forums/blog.php?b=60). Поэтому оба листинга полезно сравнить буквально пооператорно, определяя моменты сходства и отличия между "большим" Excel и "маленьким" OWC. Очень интересен в этом отношении способ гашения итогов: при визуальной схожести фрагментов "Subtotals(1, false)" они обнаружили заметное различие по смыслу. В Excel первый параметр - одно из значений перечисления: 1-Automatic, 2-Sum, 3-Count,..., 11-Var,12-Varp. В OWC PivotTable - "Must be 1" (см.комментарий и ссылку в коде).

#CCADO
static void Job_showUserGroupInActiveXPivot(Args _args)
{
    Form                form = new Form();
    FormBuildDesign     formBuildDesign = form.addDesign('Design');
    Args                args = new Args();
    FormRun             formRun;
    FormActiveXControl  pivotTable;

    UserGroupList       userGroupList;
    UserInfo            userInfo;
    UserGroupInfo       userGroupInfo;

    COM                 rst,flds,fld;   // переменные ADO

    COM                 ptConstants;    // переменные PivotTable
    COM                 activeView;
    COM                 avFieldSets, fieldSet;
    COM                 pivotFields, pivotField, pivotTotal;
    COM                 comTemp;

    void processFieldSet(COM _axis, str _fieldSetName)
    {
        fieldSet = avFieldSets.Item(_fieldSetName);
        _axis.InsertFieldSet(fieldSet);     // добавляем FieldSet в ось (т.е в Строки или в Столбцы)

        pivotFields = fieldSet.Fields();    // в семействе полей FieldSet'а...
        pivotField = pivotFields.Item(0);   // ...находим единственное поле... (индексация начинается с 0)
        pivotField.Subtotals(1, false);     // ...и гасим (false) для него промежуточные итоги
        // по поводу 1 см. "Must be 1" в [url]http://msdn.microsoft.com/en-us/library/aa831714(office.10).aspx[/url]
    }
    ;

// готовим рекордсет
    rst = AdoRst::openRecordsetInMemory([   // метод можно взять здесь: [url]http://www.axforum.info/forums/blog.php?b=60[/url]
            ['UserId'    , #adVarChar,  5 ],
            ['UserName'  , #adVarChar, 40 ],
            ['GroupId'   , #adVarChar, 10 ],
            ['GroupName' , #adVarChar, 40]]);

    flds = rst.Fields();

    while select userGroupList
        join userInfo
            where userInfo.id == userGroupList.userId
        join userGroupInfo
            where userGroupInfo.id == userGroupList.groupId
    {
        rst.AddNew();
            fld = flds.Item('UserId'   ); fld.Value(userGroupList.userId );
            fld = flds.Item('UserName' ); fld.Value(userInfo.name        );
            fld = flds.Item('GroupId'  ); fld.Value(userGroupList.groupId);
            fld = flds.Item('GroupName'); fld.Value(userGroupInfo.name   );
        rst.Update();
    }

// динамически генерируем форму
    args.object(form);

    formRun = classFactory.formRunClass(args);
    formRun.init();
    formRun.design().caption('Сводная таблица "Пользователи-Группы"');

    pivotTable = formRun.design().addControl(FormControlType::ActiveX, 'PivotTable');
    pivotTable.className('{0002E542-0000-0000-C000-000000000046}'); // Microsoft Office PivotTable 10.0

    pivotTable.heightMode(FormHeight::ColumnHeight);    // именно heightMode ! не путать с просто height
    pivotTable.widthMode(FormWidth::ColumnWidth);       // именно widthMode  ! не путать с просто width
    pivotTable.AutoFit(false);

    ptConstants = pivotTable.Constants(); // для использования ниже именованной константы plFunctionCount

// передаем рекордсет ActiveX'у
    pivotTable.DataSource(rst);


    activeView = pivotTable.ActiveView();
    avFieldSets = activeView.FieldSets(); // список доступных полей рекордсета для размещения в сводной таблице

// сокрытие разных "визуальных раздражителей" (здесь хорошо помог \Classes\GM_PivotViewManager\ initDefaultViewProperties от GMCS)
    COM::createFromObject( pivotTable.ActiveData() ).HideDetails();     // это лучше сделать до размещения полей и особенно итогов - иначе летаргически долго
    COM::createFromObject( activeView.TitleBar()   ).Visible(false);    // выключение синего заголовка "Сводная таблица MS Office 10.0"
    comTemp = ActiveView.FilterAxis();
    COM::createFromObject( comTemp.Label() ).Visible(false);            // выключение области "Перетащите сюда поля фильтра"
    pivotTable.AllowDetails(false);                                     // чтобы убрались "+/-" и не получать надпись "Нет деталей"
    pivotTable.DisplayOfficeLogo(false);                                // выключение самой левой красно-сине-желто-зеленой кнопки на панели


// наполняем строки полями UserName и UserId
    processFieldSet(activeView.RowAxis(), 'UserName');
    processFieldSet(activeView.RowAxis(), 'UserId');

// наполняем столбцы полями GroupId и GroupName
    processFieldSet(activeView.ColumnAxis(), 'GroupId');
    processFieldSet(activeView.ColumnAxis(), 'GroupName');

// наполняем область данных итогом - подсчетом кол-ва по полю GroupName (это поле осталось в переменной pivotField после его вставки в столбцы)
    pivotTotal = activeView.AddTotal('Количество', pivotField, ptConstants.plFunctionCount());  // использование именованной константы
    COM::createFromObject( activeView.DataAxis() ).InsertTotal(pivotTotal);

    formRun.run();
    formRun.wait();
}
При подготовке этого сообщения я использовал следующие материалы для изучения вопроса и написания джоба:
* файл помощи OWCVBA10.CHM на своем компьютере, а также другие источники, описанные в сообщении Сводные таблицы и Olap в Dax2009 (http://www.axforum.info/forums/showpost.php?p=224567);
* демонстрационный пример Ivanhoe (http://www.axforum.info/forums/member.php?u=5030) из сообщения Сводные таблицы и Olap в Dax2009 (http://www.axforum.info/forums/showpost.php?p=224500);
* наработки компании GMCS, которые нашёл в AOT нашей корпоративной Аксапты (3.0, SP4) - классы и формы с именами, начинающимися на "GM_Pivot"; пример работающей реализации этого хозяйства можно увидеть (клиентам GMCS!) по маршруту: Управление запасами \Запросы \Оборотно-сальдовая ведомость по складу \вкладка Обзор \кнопка Сводная таблица);
* Метод \Classes\SysTableLookup\formRun - для подсматривания простейшего способа создания динамической формы без использования класса Dialog.

]]></description>
			<content:encoded><![CDATA[<div>Сразу определимся с терминами. &quot;Сводная таблица на форме&quot; - это Office Web Component PivotTable, размещенный как элемент управления ActiveX на форме Аксапты. &quot;ADODB.Recordset&quot; - в первую (и наиболее интересную для нас) очередь отсоединенный (или неприсоединенный, или, если угодно, disconnected), в общем, не связанный с базой данных набор записей, формируемый программистом в оперативной памяти (процесс во многом напоминает заполнение массива: <a href="http://www.axforum.info/forums/showpost.php?p=224083" target="_blank">Вывод в Excel через Array</a>). Работа с отсоединенным ADODB.Recordset подробно описана в теме <a href="http://www.axforum.info/forums/showthread.php?t=12973" target="_blank">Поговорим об ADO</a>, а также в ряде других смежных обсуждений (<a href="http://axforum.info/forums/showthread.php?t=27234" target="_blank">Строка в Excel</a>, <a href="http://axforum.info/forums/showthread.php?t=16714" target="_blank">экспорт в шаблон excel</a> и т.п.).<br />
<br />
До недавнего времени я не знал о существовании возможности подачи данных в OWC PivotTable из отсоединенного Recordset'а, будучи, тем не менее, в курсе подобной возможности для сводной таблицы в Excel через PivoteCache (<a href="http://www.axforum.info/forums/blog.php?b=60" target="_blank">Вывод в Excel сводной таблицы &quot;пользователи-группы&quot;</a>).  В том своем сообщении я очень сожалел об отсутствии объекта PivotCache у OWC PivotTable. И думал, что данные могут поступать только из Recordset'ов, связанных с реальными таблицами БД. <br />
<br />
Оказалась, что возможность загрузки из неприсоединенного Recordset'а всё-таки существует! Не в том смысле, что нашёлся PivotCache у PivotTable - его как не было, так и нет - и это на сегодняшний день медицинский факт. Но в том смысле, что загружать данные всё-таки можно, хотя и несколько идеологически иначе - при помощи метода PivotTable.DataSource( recordset ): см. <a href="http://axforum.info/forums/showthread.php?t=33020" target="_blank">Сводные таблицы и Olap в Dax2009</a>.<br />
<br />
Приводимый ниже джоб - мой первый опыт работы с OWC PivoteTable на форме. Поэтому можно говорить о том, что публикую я его больше как узелок на память для самого себя, нежели как &quot;лекцию&quot; по пивотостроению (хм, и здесь пиво... :) ).<br />
<br />
Джоб решает по сути абсолютно ту же задачу, что и джоб из сообщения <a href="http://www.axforum.info/forums/blog.php?b=60" target="_blank">Вывод в Excel сводной таблицы &quot;пользователи-группы&quot;</a>. Поэтому оба листинга полезно сравнить буквально пооператорно, определяя моменты сходства и отличия между &quot;большим&quot; Excel и &quot;маленьким&quot; OWC. Очень интересен в этом отношении способ гашения итогов: при визуальной схожести фрагментов &quot;Subtotals(1, false)&quot; они обнаружили заметное различие по смыслу. В Excel первый параметр - одно из значений перечисления: 1-Automatic, 2-Sum, 3-Count,..., 11-Var,12-Varp. В OWC PivotTable - &quot;Must be 1&quot; (см.комментарий и ссылку в коде).<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">#CCADO
<span style="color: blue">static</span> <span style="color: blue">void</span> Job_showUserGroupInActiveXPivot(Args _args)
{
    Form                form = <span style="color: blue">new</span> Form();
    FormBuildDesign     formBuildDesign = form.addDesign(<span style="color: red">'Design'</span>);
    Args                args = <span style="color: blue">new</span> Args();
    FormRun             formRun;
    FormActiveXControl  pivotTable;

    UserGroupList       userGroupList;
    UserInfo            userInfo;
    UserGroupInfo       userGroupInfo;

    COM                 rst,flds,fld;   <span style="color: green">// переменные ADO
</span>
    COM                 ptConstants;    <span style="color: green">// переменные PivotTable
</span>    COM                 activeView;
    COM                 avFieldSets, fieldSet;
    COM                 pivotFields, pivotField, pivotTotal;
    COM                 comTemp;

    <span style="color: blue">void</span> processFieldSet(COM _axis, <span style="color: blue">str</span> _fieldSetName)
    {
        fieldSet = avFieldSets.Item(_fieldSetName);
        _axis.InsertFieldSet(fieldSet);     <span style="color: green">// добавляем FieldSet в ось (т.е в Строки или в Столбцы)
</span>
        pivotFields = fieldSet.Fields();    <span style="color: green">// в семействе полей FieldSet'а...
</span>        pivotField = pivotFields.Item(0);   <span style="color: green">// ...находим единственное поле... (индексация начинается с 0)
</span>        pivotField.Subtotals(1, <span style="color: blue">false</span>);     <span style="color: green">// ...и гасим (false) для него промежуточные итоги
</span>        <span style="color: green">// по поводу 1 см. &quot;Must be 1&quot; в [url]http://msdn.microsoft.com/en-us/library/aa831714(office.10).aspx[/url]
</span>    }
    ;

<span style="color: green">// готовим рекордсет
</span>    rst = AdoRst::openRecordsetInMemory([   <span style="color: green">// метод можно взять здесь: [url]http://www.axforum.info/forums/blog.php?b=60[/url]
</span>            [<span style="color: red">'UserId'</span>    , #adVarChar,  5 ],
            [<span style="color: red">'UserName'</span>  , #adVarChar, 40 ],
            [<span style="color: red">'GroupId'</span>   , #adVarChar, 10 ],
            [<span style="color: red">'GroupName'</span> , #adVarChar, 40]]);

    flds = rst.Fields();

    <span style="color: blue">while</span> <span style="color: blue">select</span> userGroupList
        <span style="color: blue">join</span> userInfo
            <span style="color: blue">where</span> userInfo.id == userGroupList.userId
        <span style="color: blue">join</span> userGroupInfo
            <span style="color: blue">where</span> userGroupInfo.id == userGroupList.groupId
    {
        rst.AddNew();
            fld = flds.Item(<span style="color: red">'UserId'</span>   ); fld.Value(userGroupList.userId );
            fld = flds.Item(<span style="color: red">'UserName'</span> ); fld.Value(userInfo.name        );
            fld = flds.Item(<span style="color: red">'GroupId'</span>  ); fld.Value(userGroupList.groupId);
            fld = flds.Item(<span style="color: red">'GroupName'</span>); fld.Value(userGroupInfo.name   );
        rst.Update();
    }

<span style="color: green">// динамически генерируем форму
</span>    args.object(form);

    formRun = classFactory.formRunClass(args);
    formRun.init();
    formRun.design().caption(<span style="color: red">'Сводная таблица &quot;Пользователи-Группы&quot;'</span>);

    pivotTable = formRun.design().addControl(FormControlType::ActiveX, <span style="color: red">'PivotTable'</span>);
    pivotTable.className(<span style="color: red">'{0002E542-0000-0000-C000-000000000046}'</span>); <span style="color: green">// Microsoft Office PivotTable 10.0
</span>
    pivotTable.heightMode(FormHeight::ColumnHeight);    <span style="color: green">// именно heightMode ! не путать с просто height
</span>    pivotTable.widthMode(FormWidth::ColumnWidth);       <span style="color: green">// именно widthMode  ! не путать с просто width
</span>    pivotTable.AutoFit(<span style="color: blue">false</span>);

    ptConstants = pivotTable.Constants(); <span style="color: green">// для использования ниже именованной константы plFunctionCount
</span>
<span style="color: green">// передаем рекордсет ActiveX'у
</span>    pivotTable.DataSource(rst);


    activeView = pivotTable.ActiveView();
    avFieldSets = activeView.FieldSets(); <span style="color: green">// список доступных полей рекордсета для размещения в сводной таблице
</span>
<span style="color: green">// сокрытие разных &quot;визуальных раздражителей&quot; (здесь хорошо помог \Classes\GM_PivotViewManager\ initDefaultViewProperties от GMCS)
</span>    COM::createFromObject( pivotTable.ActiveData() ).HideDetails();     <span style="color: green">// это лучше сделать до размещения полей и особенно итогов - иначе летаргически долго
</span>    COM::createFromObject( activeView.TitleBar()   ).Visible(<span style="color: blue">false</span>);    <span style="color: green">// выключение синего заголовка &quot;Сводная таблица MS Office 10.0&quot;
</span>    comTemp = ActiveView.FilterAxis();
    COM::createFromObject( comTemp.Label() ).Visible(<span style="color: blue">false</span>);            <span style="color: green">// выключение области &quot;Перетащите сюда поля фильтра&quot;
</span>    pivotTable.AllowDetails(<span style="color: blue">false</span>);                                     <span style="color: green">// чтобы убрались &quot;+/-&quot; и не получать надпись &quot;Нет деталей&quot;
</span>    pivotTable.DisplayOfficeLogo(<span style="color: blue">false</span>);                                <span style="color: green">// выключение самой левой красно-сине-желто-зеленой кнопки на панели
</span>

<span style="color: green">// наполняем строки полями UserName и UserId
</span>    processFieldSet(activeView.RowAxis(), <span style="color: red">'UserName'</span>);
    processFieldSet(activeView.RowAxis(), <span style="color: red">'UserId'</span>);

<span style="color: green">// наполняем столбцы полями GroupId и GroupName
</span>    processFieldSet(activeView.ColumnAxis(), <span style="color: red">'GroupId'</span>);
    processFieldSet(activeView.ColumnAxis(), <span style="color: red">'GroupName'</span>);

<span style="color: green">// наполняем область данных итогом - подсчетом кол-ва по полю GroupName (это поле осталось в переменной pivotField после его вставки в столбцы)
</span>    pivotTotal = activeView.AddTotal(<span style="color: red">'Количество'</span>, pivotField, ptConstants.plFunctionCount());  <span style="color: green">// использование именованной константы
</span>    COM::createFromObject( activeView.DataAxis() ).InsertTotal(pivotTotal);

    formRun.run();
    formRun.wait();
}</pre></div>При подготовке этого сообщения я использовал следующие материалы для изучения вопроса и написания джоба:<ul><li>файл помощи OWCVBA10.CHM на своем компьютере, а также другие источники, описанные в сообщении <a href="http://www.axforum.info/forums/showpost.php?p=224567" target="_blank">Сводные таблицы и Olap в Dax2009</a>;</li>
<li>демонстрационный пример <a href="http://www.axforum.info/forums/member.php?u=5030" target="_blank">Ivanhoe</a> из сообщения <a href="http://www.axforum.info/forums/showpost.php?p=224500" target="_blank">Сводные таблицы и Olap в Dax2009</a>;</li>
<li>наработки компании GMCS, которые нашёл в AOT нашей корпоративной Аксапты (3.0, SP4) - классы и формы с именами, начинающимися на &quot;GM_Pivot&quot;; пример работающей реализации этого хозяйства можно увидеть (клиентам GMCS!) по маршруту: Управление запасами \Запросы \Оборотно-сальдовая ведомость по складу \вкладка Обзор \кнопка Сводная таблица);</li>
<li>Метод \Classes\SysTableLookup\formRun - для подсматривания простейшего способа создания динамической формы без использования класса Dialog.</li>
</ul></div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=120</guid>
		</item>
		<item>
			<title>Заканчивается поддержка: Windows 2000,  XP SP2 and Vista End of Life Support</title>
			<link>//axforum.info/forums/blog.php?b=95</link>
			<pubDate>Thu, 18 Mar 2010 11:42:33 GMT</pubDate>
			<description><![CDATA[Размещено по просьбе Microsoft

Hello MVP’s,

As you may be aware, there are a number of Microsoft Windows versions which will go out of support during the coming year. We want to help customers avoid the risk of running unsupported products in their environment. Please help us share the message below on your blog, twitter or Facebook account:

Windows 2000 Professional and Windows 2000 Server are approaching 10 years since their launch and both products will go out of support on July 13, 2010. 

Windows XP was launched back in 2001. While support for the product will continue, Service Pack 2 will go out of support on July 13, 2010. From that date onwards, Microsoft will no longer support or provide free security updates for Windows XP SP2.  Please install the free Service Pack 3 for Windows XP to have the most secure and supported Windows XP platform.

Finally, Windows Vista with no Service Packs installed will end support on April 13 2010.  Please install the free Service Pack 2 for Windows Vista to have the most secure and supported Windows Vista platform.

Please help us spread this message via your blog/twitter/Facebook!


For more information:
http://blogs.technet.com/lifecycle/archive/2010/02/24/end-of-support-for-windows-xp-sp2-and-windows-vista-with-no-service-packs-installed.aspx 

Resources:
Service Pack 3 for Windows XP:
http://www.microsoft.com/downloads/details.aspx?FamilyID=68C48DAD-BC34-40BE-8D85-6BB4F56F5110&displaylang=en 

Service Pack 2 for Windows Vista: http://www.microsoft.com/windows/windows-vista/default.aspx]]></description>
			<content:encoded><![CDATA[<div>Размещено по просьбе Microsoft<br />
<br />
Hello MVP’s,<br />
<br />
As you may be aware, there are a number of Microsoft Windows versions which will go out of support during the coming year. We want to help customers avoid the risk of running unsupported products in their environment. Please help us share the message below on your blog, twitter or Facebook account:<br />
<br />
Windows 2000 Professional and Windows 2000 Server are approaching 10 years since their launch and both products will go out of support on July 13, 2010. <br />
<br />
Windows XP was launched back in 2001. While support for the product will continue, Service Pack 2 will go out of support on July 13, 2010. From that date onwards, Microsoft will no longer support or provide free security updates for Windows XP SP2.  Please install the free Service Pack 3 for Windows XP to have the most secure and supported Windows XP platform.<br />
<br />
Finally, Windows Vista with no Service Packs installed will end support on April 13 2010.  Please install the free Service Pack 2 for Windows Vista to have the most secure and supported Windows Vista platform.<br />
<br />
Please help us spread this message via your blog/twitter/Facebook!<br />
<br />
<br />
For more information:<br />
<a href="http://blogs.technet.com/lifecycle/archive/2010/02/24/end-of-support-for-windows-xp-sp2-and-windows-vista-with-no-service-packs-installed.aspx" target="_blank">http://blogs.technet.com/lifecycle/a...installed.aspx</a> <br />
<br />
Resources:<br />
Service Pack 3 for Windows XP:<br />
<a href="http://www.microsoft.com/downloads/details.aspx?FamilyID=68C48DAD-BC34-40BE-8D85-6BB4F56F5110&amp;displaylang=en" target="_blank">http://www.microsoft.com/downloads/d...displaylang=en</a> <br />
<br />
Service Pack 2 for Windows Vista: <a href="http://www.microsoft.com/windows/windows-vista/default.aspx" target="_blank">http://www.microsoft.com/windows/win...a/default.aspx</a></div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=95</guid>
		</item>
		<item>
			<title>Оперативная сортировка по убыванию</title>
			<link>//axforum.info/forums/blog.php?b=91</link>
			<pubDate>Mon, 15 Mar 2010 07:40:56 GMT</pubDate>
			<description><![CDATA[Программисту-аксаптоведу известно, что если поместить набор однотипных элементов в базовый класс Set ("множество"), а потом выбрать их оттуда при помощи итератора (SetIterator) или перечислителя (SetEnumerator), то порядок извлечения будет соответствовать порядку сортировки этих элементов по возрастанию.

Слово "оперативная" в заголовке темы относится скорее даже не к выполнению процесса в оперативной памяти, а к возможности выполнить такую сортировку "быстренько, на ходу", не прибегая к более тяжелой "артиллерии" - например, временным таблицам.

Итак, использование Set позволяет нам получить список значений, отсортированный в возрастающем порядке (A..Z).  Но как быть, если требуется сортировка по убыванию (Z..A)? 

При сортировке чисел - int или real - можно использовать прием, о котором я уже как-то рассказывал в теме order by и group by (http://www.axforum.info/forums/showthread.php?p=152998). Суть приема заключается во врЕменном изменении знака числа на противоположный при добавлении в Set и в восстановлении знака при извлечении из Set'а.

static void Job_Numeric(Args _args)
{
    Set             set = new Set(Types::Integer);
    SetEnumerator   setEnumerator;
    int             i;
    int             t;

    t = WinAPI::getTickCount();
//--------------------------------------------
    for (i=1; i<=9000; i++)
        set.add(-i);

    setEnumerator = set.getEnumerator();
    while (setEnumerator.moveNext())
        i = -1*setEnumerator.current();
//--------------------------------------------
    info(strFmt('Milliseconds: %1', WinAPI::getTickCount()-t));

    setEnumerator.reset();
    while (setEnumerator.moveNext())
        info(strFmt('%1', -1*setEnumerator.current()));
}
Последовательность операторов между пунктирными линиями (или, что то же самое, между засекающими время вызовами getTickCount) собственно и решает задачу сортировки. Вывод отсортированных по убыванию элементов множества в окно Infolog вынесен за пределы этого фрагмента из-за продолжительного времени операции вывода, многократно превышающего время отрабатывания непосредственно сортирующих операторов.

Пусть вас не смущает выглядящая несколько странной операция присваивания i = -1*setEnumerator.current(). Она здесь лишь для того, чтобы показать, как следует восстанавливать исходные значения чисел, а также для учёта времени, затрачиваемого на восстановление, в общем времени сортировки. Вы можете видеть, что чуть ниже цикла с этим фиктивным присваиванием цикл перебора множества повторяется еще раз, но теперь уже исключительно для вывода значений в окно Infolog. Аналогичный подход принят и в следующих демонстрационных джобах, при рассмотрении сортировок других типов данных.

Переходим к обратной сортировке дат. С ними - чуть сложнее, чем с числами. Их нельзя просто так взять со знаком минус, но можно вычесть из одной и той же заведомо большей (более поздней) даты. Получившиеся разницы в днях (целые положительные числа) можно загрузить в Set - внимание: типа Integer, а не Date! В качестве очень большой даты можно использовать значение, возвращаемое функцией dateMax() = 31.12.2153 (по крайней мере для сортировок в течение ближайших ста сорока лет нам ее должно хватить :))

static void Job_Date(Args _args)
{
    Set             set = new Set(Types::Integer);
    SetEnumerator   setEnumerator;
    date            d, dMax=dateMax(), td=today(), tdMin=td-9000;
    int             t;

    t = WinAPI::getTickCount();
//--------------------------------------------
    for (d=td; d>tdMin; d--)
        set.add(dMax-d);

    setEnumerator = set.getEnumerator();
    while (setEnumerator.moveNext())
        d = dMax-setEnumerator.current();
//--------------------------------------------
    info(strFmt('Milliseconds: %1', WinAPI::getTickCount()-t));

    setEnumerator.reset();
    while (setEnumerator.moveNext())
        info(strFmt('%1', dMax-setEnumerator.current()));
}
Теперь о текстовых строках. Сразу отмечу, что обратно-сортировочное решение для данных этого типа ко мне шло как-то очень долго. Временами в голове рисовались какие-то побайтные инвертирования и прочие головоломки. Я всячески гнал эти мысли от себя и, как выяснилось, поступил правильно, когда, наконец, вдруг вспомнилось, что в базовый класс List элементы можно добавлять как в конец, так и в начало списка! (Ну, не часто я использую List в своей практике, ну, извиняйте!)

static void Job_String(Args _args)
{
    Set             set = new Set(Types::String);
    SetEnumerator   setEnumerator;
    str             s;
    int             t, i;
    List            list = new List(Types::String);
    ListEnumerator  listEnumerator;

    t = WinAPI::getTickCount();
//--------------------------------------------
    for (i=1; i<=9000; i++)
        set.add('Строка '+subStr(int2str(10000+i),2,4));

    setEnumerator = set.getEnumerator();
    while (setEnumerator.moveNext())
        list.addStart(setEnumerator.current());

    listEnumerator = list.getEnumerator();
    while (listEnumerator.moveNext())
        s = listEnumerator.current();
//--------------------------------------------
    info(strFmt('Milliseconds: %1', WinAPI::getTickCount()-t));

    listEnumerator.reset();
    while (listEnumerator.moveNext())
        info(strFmt('%1', listEnumerator.current()));
}
Как видно из кода Job_String, добавился еще один шаг - загрузка строк, отсортированных в Set, в List в обратном порядке. Из этого List'а теперь и получается окончательный результат. Несмотря на дополнительный шаг, процедура для строк претендует на статус универсальной, пригодной для данных любого типа. 

Тем не менее, продемонстрированные выше частные решения для чисел и дат не будем спешить удалять из своего арсенала. Причина - бОльшая компактность кода, а также вполне логично объяснимая более высокая скорость выполнения. Вы можете сами поэкспериментировать, сравнивая количество миллисекунд выполнения частных и универсальных версий. Для удобства сравнения вот еще два джобика для чисел и дат в универсальном формате:

static void Job_Numeric_Univers(Args _args)
{
    Set             set = new Set(Types::Integer);
    SetEnumerator   setEnumerator;
    int             i;
    int             t;
    List            list = new List(Types::Integer);
    ListEnumerator  listEnumerator;

    t = WinAPI::getTickCount();
//--------------------------------------------
    for (i=1; i<=9000; i++)
        set.add(i);

    setEnumerator = set.getEnumerator();
    while (setEnumerator.moveNext())
        list.addStart(setEnumerator.current());

    listEnumerator = list.getEnumerator();
    while (listEnumerator.moveNext())
        i = listEnumerator.current();
//--------------------------------------------
    info(strFmt('Milliseconds: %1', WinAPI::getTickCount()-t));

    listEnumerator.reset();
    while (listEnumerator.moveNext())
        info(strFmt('%1', listEnumerator.current()));
}

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

static void Job_Date_Univers(Args _args)
{
    Set             set = new Set(Types::Date);
    SetEnumerator   setEnumerator;
    date            d, td=today(), tdMin=td-9000;
    int             t;
    List            list = new List(Types::Date);
    ListEnumerator  listEnumerator;

    t = WinAPI::getTickCount();
//--------------------------------------------
    for (d=td; d>tdMin; d--)
        set.add(d);

    setEnumerator = set.getEnumerator();
    while (setEnumerator.moveNext())
        list.addStart(setEnumerator.current());

    listEnumerator = list.getEnumerator();
    while (listEnumerator.moveNext())
        d = listEnumerator.current();
//--------------------------------------------
    info(strFmt('Milliseconds: %1', WinAPI::getTickCount()-t));

    listEnumerator.reset();
    while (listEnumerator.moveNext())
        info(strFmt('%1', listEnumerator.current()));
}
У меня лично в среднем получилось: для чисел - 100 ms в частном случае против 170 ms универсального решения; для дат - 140 и 170 ms соответственно.

А вот и "сладкое" - переменная сортировка контейнеров. Это из серии "Когда верстался номер", потому что, начиная писать эту заметку, я настолько же не думал о контейнерах, насколько был очень рад наконец появившемуся решению для строк.

Как бы то ни было, но аппетит, как говорится, пришёл во время еды, и в результате следующий джоб решает задачу комплексной сортировки набора контейнеров, состоящих из пяти элементов (для удобства изложения будем называть их полями - по аналогии со строкой таблицы БД, на которую контейнер вполне похож). Поля контейнеров в примере имеют типы: 1 - действительное, 2 - дата, 3,4,5 - строки. Требуемый порядок сортировки: 1,2 - по возрастанию, 3 - по убыванию, 4 - по возрастанию, 5 - по убыванию.

Мы сосредоточимся на интересной для нас обратной сортировке, которая "начинается" в глубине контейнера - с 3-го поля. Для того чтобы два первых поля при этом не "фонили", заполним их одинаковыми повторяющимися значениями (3.62 и 31\12\2009).

Вспомогательные действия и врЕменные изменения, которые нужно проделать с набором контейнеров при желании отсортировать некоторые текстовые поля по убыванию:
* определить общее количество контейнеров в наборе (элементов в Set) и найти ближайшее число "10 в степени x", превышающее это количество (x - положительное целое число);
.
* переставить поля в контейнерах так, чтобы на первых местах слева оказались поля, по которым хочется выполнить сортировку по убыванию;
.
* создать еще один вспомогательный набор (SetTmp), который заполнить переставленными контейнерами. При этом контейнеры автоматически отсортируются, правда, пока по возрастанию;
.
* очистить исходный набор (Set) и заполнить его контейнерами из вспомогательного набора, возвращая всем полям контейнеров исходный порядок и дописывая в начало значений из сортируемых по убыванию полей подстроку, состоящую из x-значного цифрового кода, рассчитываемого по убыванию, начиная со значения 10^x-1 ("99..99"). К повторяющимся исходным значениям текстовых полей добавляются и повторяющиеся (одинаковые) цифровые коды;
.
* окончательно прочитать контейнеры из Set, восстанавливая исходные текстовые значения путем обрезания слева x временно добавленных символов.

Перечисленные алгоритмические шаги реализованы в джобе:

static void Job_Container_AscDesc(Args _args)
{
// СОРТИРОВКА КОНТЕЙНЕРОВ В ВОЗРАСТАЮЩЕ-УБЫВАЮЩЕМ ПОРЯДКЕ
// Имеется множество контейнеров вида [r1,d2,s3,s4,s5] (real,date,str,str,str)
// Нужно выполнить сортировку: r1 ASC, d2 ASC, s3 DESC, s4 ASC, s5 DESC

    Set             set     = new Set(Types::Container);
    Set             setTmp  = new Set(Types::Container);
    SetEnumerator   setEnumerator;
    real            r1;
    date            d2;
    str             s3, s4, s5, sprev, s3prev, s5prev;
    int             t, i, j, k, pwr, tenInPwr;
    ;

    t = WinAPI::getTickCount();
//--------------------------------------------
    // Заполнение Set исходным набором данных - контейнерами [r1,d2,s3,s4,s5]
    for (i=1; i<=20; i++)
        for (j=1; j<=20; j++)
            for (k=1; k<=20; k++) // итого 20 * 20 * 20 = 8000 элементов
                set.add([3.62, 31\12\2009,
                        'Строка '+subStr(int2str(100+i),2,2),
                        'Строка '+subStr(int2str(100+j),2,2),
                        'Строка '+subStr(int2str(100+k),2,2)]);

    // вычисляем кол-во элементов в Set и число 10^x, достаточное для их нумерации
    pwr = trunc(log10(set.elements()))+1; // х
    tenInPwr = any2int(power(10,pwr));    // 10^x

    // 1 => 2: перебираем Set и заполняем вспомогательный SetTmp контейнерами с переставленными элементами
    setEnumerator = set.getEnumerator();
    while (setEnumerator.moveNext())
    {
        [r1,d2,s3,s4,s5] = setEnumerator.current();
        // выводим на первые позиции контейнера все сортируемые по убыванию строковые поля - s3, s5
        setTmp.add([s3,s5, r1,d2,s4]);
    }

    //  2 => 1: очищаем Set и заполняем его из SetTmp
    // строками с добавленными в начало "номерами" для изменения порядка сортировки
    set = new Set(Types::Container);
    i = tenInPwr;
    j = tenInPwr;
    s3prev = strMax(); 
    s5prev = strMax();
    setEnumerator = setTmp.getEnumerator();
    while (setEnumerator.moveNext())
    {
        [s3,s5, r1,d2,s4] = setEnumerator.current(); // например, s3 = "Вася"

        // первое убывающее текстовое поле
        if (s3 != s3prev) // "номер" в s3 изменяем только при изменении значения
        {
            i--;
            s3prev = s3;
        }
        s3 = subStr( int2str(tenInPwr+i), 2, pwr) + s3; // например, s3 станет = "9999Вася"

        // второе убывающее текстовое поле
        if (s5 != s5prev) // "номер" в s5 изменяем только при изменении значения
        {
            j--;
            s5prev = s5;
        }
        s5 = subStr( int2str(tenInPwr+j), 2, pwr) + s5;

        // здесь также можно преобразовать нетекстовые убывающие поля (если есть такие)

        // восстановление исходного порядка элементов в контейнере
        set.add([r1,d2,s3,s4,s5]);
    }

    // окончательное извлечение и восстановление измененных значений
    setEnumerator = set.getEnumerator();
    while (setEnumerator.moveNext())
    {
        [r1,d2,s3,s4,s5] = setEnumerator.current();
        s3 = subStr(s3, pwr+1, strLen(s3)); // "9999Вася" => "Вася"
        s5 = subStr(s5, pwr+1, strLen(s5));
        // здесь также восстанавливаем нетекстовые убывающие поля (если есть)
    }
//--------------------------------------------
    info(strFmt('Milliseconds: %1', WinAPI::getTickCount()-t));

    setEnumerator.reset();
    while (setEnumerator.moveNext())
    {
        [r1,d2,s3,s4,s5] = setEnumerator.current();
        s3 = subStr(s3, pwr+1, strLen(s3));
        s5 = subStr(s5, pwr+1, strLen(s5));

        info(strFmt('%1 -- %2 -- %3 -- %4 -- %5', r1,d2,s3,s4,s5));
    }
}
Если помимо обратной сортировки по текстовым полям требуется также и обратная сортировка по полям чисел или дат, то для них манипуляции существенно проще: их не надо переставлять внутри контейнера, достаточно лишь "изменить знак" перед окончательной ("сортирующей") загрузкой в Set и затем в процессе считывания восстановить значения повторным "изменением знака". Потенциальные места преобразования нетекстовых полей в джобе отмечены соответствующими комментариями.

Используя сортирующую способность Set, не следует забывать и о его группирующей способности - содержать в себе только уникальные значения, игнорируя повторное добавление уже имеющихся элементов. Поэтому если сортируется набор неуникальных простых значений (чисел, дат, строк), их следует превратить в контейнеры путем добавления на вторую позицию уникального значения целочисленного счетчика (своеобразного "RecId"), как это было сделано, например, здесь: order by и group by (http://www.axforum.info/forums/showthread.php?p=152998) (в результате чего оба значения "200" попали в финальный результат). Если сортируются неуникальные контейнеры, то их так же надо сделать уникальными, добавив аналогичный "RecId" последним элементом.]]></description>
			<content:encoded><![CDATA[<div>Программисту-аксаптоведу известно, что если поместить набор однотипных элементов в базовый класс Set (&quot;множество&quot;), а потом выбрать их оттуда при помощи итератора (SetIterator) или перечислителя (SetEnumerator), то порядок извлечения будет соответствовать порядку сортировки этих элементов по возрастанию.<br />
<br />
Слово &quot;оперативная&quot; в заголовке темы относится скорее даже не к выполнению процесса в оперативной памяти, а к возможности выполнить такую сортировку &quot;быстренько, на ходу&quot;, не прибегая к более тяжелой &quot;артиллерии&quot; - например, временным таблицам.<br />
<br />
Итак, использование Set позволяет нам получить список значений, отсортированный в возрастающем порядке (A..Z).  Но как быть, если требуется сортировка по убыванию (Z..A)? <br />
<br />
При сортировке чисел - int или real - можно использовать прием, о котором я уже как-то рассказывал в теме <a href="http://www.axforum.info/forums/showthread.php?p=152998" target="_blank">order by и group by</a>. Суть приема заключается во врЕменном изменении знака числа на противоположный при добавлении в Set и в восстановлении знака при извлечении из Set'а.<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">static</span> <span style="color: blue">void</span> Job_Numeric(Args _args)
{
    Set             set = <span style="color: blue">new</span> Set(Types::Integer);
    SetEnumerator   setEnumerator;
    <span style="color: blue">int</span>             i;
    <span style="color: blue">int</span>             t;

    t = WinAPI::getTickCount();
<span style="color: green">//--------------------------------------------
</span>    <span style="color: blue">for</span> (i=1; i&lt;=9000; i++)
        set.add(-i);

    setEnumerator = set.getEnumerator();
    <span style="color: blue">while</span> (setEnumerator.moveNext())
        i = -1*setEnumerator.current();
<span style="color: green">//--------------------------------------------
</span>    info(strFmt(<span style="color: red">'Milliseconds: %1'</span>, WinAPI::getTickCount()-t));

    setEnumerator.reset();
    <span style="color: blue">while</span> (setEnumerator.moveNext())
        info(strFmt(<span style="color: red">'%1'</span>, -1*setEnumerator.current()));
}</pre></div>Последовательность операторов между пунктирными линиями (или, что то же самое, между засекающими время вызовами getTickCount) собственно и решает задачу сортировки. Вывод отсортированных по убыванию элементов множества в окно Infolog вынесен за пределы этого фрагмента из-за продолжительного времени операции вывода, многократно превышающего время отрабатывания непосредственно сортирующих операторов.<br />
<br />
Пусть вас не смущает выглядящая несколько странной операция присваивания i = -1*setEnumerator.current(). Она здесь лишь для того, чтобы показать, как следует восстанавливать исходные значения чисел, а также для учёта времени, затрачиваемого на восстановление, в общем времени сортировки. Вы можете видеть, что чуть ниже цикла с этим фиктивным присваиванием цикл перебора множества повторяется еще раз, но теперь уже исключительно для вывода значений в окно Infolog. Аналогичный подход принят и в следующих демонстрационных джобах, при рассмотрении сортировок других типов данных.<br />
<br />
Переходим к обратной сортировке дат. С ними - чуть сложнее, чем с числами. Их нельзя просто так взять со знаком минус, но можно вычесть из одной и той же заведомо большей (более поздней) даты. Получившиеся разницы в днях (целые положительные числа) можно загрузить в Set - внимание: типа Integer, а не Date! В качестве очень большой даты можно использовать значение, возвращаемое функцией dateMax() = 31.12.2153 (по крайней мере для сортировок в течение ближайших ста сорока лет нам ее должно хватить :))<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">static</span> <span style="color: blue">void</span> Job_Date(Args _args)
{
    Set             set = <span style="color: blue">new</span> Set(Types::Integer);
    SetEnumerator   setEnumerator;
    <span style="color: blue">date</span>            d, dMax=dateMax(), td=today(), tdMin=td-9000;
    <span style="color: blue">int</span>             t;

    t = WinAPI::getTickCount();
<span style="color: green">//--------------------------------------------
</span>    <span style="color: blue">for</span> (d=td; d&gt;tdMin; d--)
        set.add(dMax-d);

    setEnumerator = set.getEnumerator();
    <span style="color: blue">while</span> (setEnumerator.moveNext())
        d = dMax-setEnumerator.current();
<span style="color: green">//--------------------------------------------
</span>    info(strFmt(<span style="color: red">'Milliseconds: %1'</span>, WinAPI::getTickCount()-t));

    setEnumerator.reset();
    <span style="color: blue">while</span> (setEnumerator.moveNext())
        info(strFmt(<span style="color: red">'%1'</span>, dMax-setEnumerator.current()));
}</pre></div>Теперь о текстовых строках. Сразу отмечу, что обратно-сортировочное решение для данных этого типа ко мне шло как-то очень долго. Временами в голове рисовались какие-то побайтные инвертирования и прочие головоломки. Я всячески гнал эти мысли от себя и, как выяснилось, поступил правильно, когда, наконец, вдруг вспомнилось, что в базовый класс List элементы можно добавлять как в конец, так и в начало списка! (Ну, не часто я использую List в своей практике, ну, извиняйте!)<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">static</span> <span style="color: blue">void</span> Job_String(Args _args)
{
    Set             set = <span style="color: blue">new</span> Set(Types::String);
    SetEnumerator   setEnumerator;
    <span style="color: blue">str</span>             s;
    <span style="color: blue">int</span>             t, i;
    List            list = <span style="color: blue">new</span> List(Types::String);
    ListEnumerator  listEnumerator;

    t = WinAPI::getTickCount();
<span style="color: green">//--------------------------------------------
</span>    <span style="color: blue">for</span> (i=1; i&lt;=9000; i++)
        set.add(<span style="color: red">'Строка '</span>+subStr(int2str(10000+i),2,4));

    setEnumerator = set.getEnumerator();
    <span style="color: blue">while</span> (setEnumerator.moveNext())
        list.addStart(setEnumerator.current());

    listEnumerator = list.getEnumerator();
    <span style="color: blue">while</span> (listEnumerator.moveNext())
        s = listEnumerator.current();
<span style="color: green">//--------------------------------------------
</span>    info(strFmt(<span style="color: red">'Milliseconds: %1'</span>, WinAPI::getTickCount()-t));

    listEnumerator.reset();
    <span style="color: blue">while</span> (listEnumerator.moveNext())
        info(strFmt(<span style="color: red">'%1'</span>, listEnumerator.current()));
}</pre></div>Как видно из кода Job_String, добавился еще один шаг - загрузка строк, отсортированных в Set, в List в обратном порядке. Из этого List'а теперь и получается окончательный результат. Несмотря на дополнительный шаг, процедура для строк претендует на статус универсальной, пригодной для данных любого типа. <br />
<br />
Тем не менее, продемонстрированные выше частные решения для чисел и дат не будем спешить удалять из своего арсенала. Причина - бОльшая компактность кода, а также вполне логично объяснимая более высокая скорость выполнения. Вы можете сами поэкспериментировать, сравнивая количество миллисекунд выполнения частных и универсальных версий. Для удобства сравнения вот еще два джобика для чисел и дат в универсальном формате:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">static</span> <span style="color: blue">void</span> Job_Numeric_Univers(Args _args)
{
    Set             set = <span style="color: blue">new</span> Set(Types::Integer);
    SetEnumerator   setEnumerator;
    <span style="color: blue">int</span>             i;
    <span style="color: blue">int</span>             t;
    List            list = <span style="color: blue">new</span> List(Types::Integer);
    ListEnumerator  listEnumerator;

    t = WinAPI::getTickCount();
<span style="color: green">//--------------------------------------------
</span>    <span style="color: blue">for</span> (i=1; i&lt;=9000; i++)
        set.add(i);

    setEnumerator = set.getEnumerator();
    <span style="color: blue">while</span> (setEnumerator.moveNext())
        list.addStart(setEnumerator.current());

    listEnumerator = list.getEnumerator();
    <span style="color: blue">while</span> (listEnumerator.moveNext())
        i = listEnumerator.current();
<span style="color: green">//--------------------------------------------
</span>    info(strFmt(<span style="color: red">'Milliseconds: %1'</span>, WinAPI::getTickCount()-t));

    listEnumerator.reset();
    <span style="color: blue">while</span> (listEnumerator.moveNext())
        info(strFmt(<span style="color: red">'%1'</span>, listEnumerator.current()));
}

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

<span style="color: blue">static</span> <span style="color: blue">void</span> Job_Date_Univers(Args _args)
{
    Set             set = <span style="color: blue">new</span> Set(Types::<span style="color: blue">Date</span>);
    SetEnumerator   setEnumerator;
    <span style="color: blue">date</span>            d, td=today(), tdMin=td-9000;
    <span style="color: blue">int</span>             t;
    List            list = <span style="color: blue">new</span> List(Types::<span style="color: blue">Date</span>);
    ListEnumerator  listEnumerator;

    t = WinAPI::getTickCount();
<span style="color: green">//--------------------------------------------
</span>    <span style="color: blue">for</span> (d=td; d&gt;tdMin; d--)
        set.add(d);

    setEnumerator = set.getEnumerator();
    <span style="color: blue">while</span> (setEnumerator.moveNext())
        list.addStart(setEnumerator.current());

    listEnumerator = list.getEnumerator();
    <span style="color: blue">while</span> (listEnumerator.moveNext())
        d = listEnumerator.current();
<span style="color: green">//--------------------------------------------
</span>    info(strFmt(<span style="color: red">'Milliseconds: %1'</span>, WinAPI::getTickCount()-t));

    listEnumerator.reset();
    <span style="color: blue">while</span> (listEnumerator.moveNext())
        info(strFmt(<span style="color: red">'%1'</span>, listEnumerator.current()));
}</pre></div>У меня лично в среднем получилось: для чисел - 100 ms в частном случае против 170 ms универсального решения; для дат - 140 и 170 ms соответственно.<br />
<br />
А вот и &quot;сладкое&quot; - переменная сортировка контейнеров. Это из серии &quot;Когда верстался номер&quot;, потому что, начиная писать эту заметку, я настолько же не думал о контейнерах, насколько был очень рад наконец появившемуся решению для строк.<br />
<br />
Как бы то ни было, но аппетит, как говорится, пришёл во время еды, и в результате следующий джоб решает задачу комплексной сортировки набора контейнеров, состоящих из пяти элементов (для удобства изложения будем называть их полями - по аналогии со строкой таблицы БД, на которую контейнер вполне похож). Поля контейнеров в примере имеют типы: 1 - действительное, 2 - дата, 3,4,5 - строки. Требуемый порядок сортировки: 1,2 - по возрастанию, 3 - по убыванию, 4 - по возрастанию, 5 - по убыванию.<br />
<br />
Мы сосредоточимся на интересной для нас обратной сортировке, которая &quot;начинается&quot; в глубине контейнера - с 3-го поля. Для того чтобы два первых поля при этом не &quot;фонили&quot;, заполним их одинаковыми повторяющимися значениями (3.62 и 31\12\2009).<br />
<br />
Вспомогательные действия и врЕменные изменения, которые нужно проделать с набором контейнеров при желании отсортировать некоторые текстовые поля по убыванию:<ul><li>определить общее количество контейнеров в наборе (элементов в Set) и найти ближайшее число &quot;10 в степени x&quot;, превышающее это количество (x - положительное целое число);<br />
.</li>
<li>переставить поля в контейнерах так, чтобы на первых местах слева оказались поля, по которым хочется выполнить сортировку по убыванию;<br />
.</li>
<li>создать еще один вспомогательный набор (SetTmp), который заполнить переставленными контейнерами. При этом контейнеры автоматически отсортируются, правда, пока по возрастанию;<br />
.</li>
<li>очистить исходный набор (Set) и заполнить его контейнерами из вспомогательного набора, возвращая всем полям контейнеров исходный порядок и дописывая в начало значений из сортируемых по убыванию полей подстроку, состоящую из x-значного цифрового кода, рассчитываемого по убыванию, начиная со значения 10^x-1 (&quot;99..99&quot;). К повторяющимся исходным значениям текстовых полей добавляются и повторяющиеся (одинаковые) цифровые коды;<br />
.</li>
<li>окончательно прочитать контейнеры из Set, восстанавливая исходные текстовые значения путем обрезания слева x временно добавленных символов.</li>
</ul>Перечисленные алгоритмические шаги реализованы в джобе:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">static</span> <span style="color: blue">void</span> Job_Container_AscDesc(Args _args)
{
<span style="color: green">// СОРТИРОВКА КОНТЕЙНЕРОВ В ВОЗРАСТАЮЩЕ-УБЫВАЮЩЕМ ПОРЯДКЕ
</span><span style="color: green">// Имеется множество контейнеров вида [r1,d2,s3,s4,s5] (real,date,str,str,str)
</span><span style="color: green">// Нужно выполнить сортировку: r1 ASC, d2 ASC, s3 DESC, s4 ASC, s5 DESC
</span>
    Set             set     = <span style="color: blue">new</span> Set(Types::<span style="color: blue">Container</span>);
    Set             setTmp  = <span style="color: blue">new</span> Set(Types::<span style="color: blue">Container</span>);
    SetEnumerator   setEnumerator;
    <span style="color: blue">real</span>            r1;
    <span style="color: blue">date</span>            d2;
    <span style="color: blue">str</span>             s3, s4, s5, sprev, s3prev, s5prev;
    <span style="color: blue">int</span>             t, i, j, k, pwr, tenInPwr;
    ;

    t = WinAPI::getTickCount();
<span style="color: green">//--------------------------------------------
</span>    <span style="color: green">// Заполнение Set исходным набором данных - контейнерами [r1,d2,s3,s4,s5]
</span>    <span style="color: blue">for</span> (i=1; i&lt;=20; i++)
        <span style="color: blue">for</span> (j=1; j&lt;=20; j++)
            <span style="color: blue">for</span> (k=1; k&lt;=20; k++) <span style="color: green">// итого 20 * 20 * 20 = 8000 элементов
</span>                set.add([3.62, 31\12\2009,
                        <span style="color: red">'Строка '</span>+subStr(int2str(100+i),2,2),
                        <span style="color: red">'Строка '</span>+subStr(int2str(100+j),2,2),
                        <span style="color: red">'Строка '</span>+subStr(int2str(100+k),2,2)]);

    <span style="color: green">// вычисляем кол-во элементов в Set и число 10^x, достаточное для их нумерации
</span>    pwr = trunc(log10(set.elements()))+1; <span style="color: green">// х
</span>    tenInPwr = any2int(power(10,pwr));    <span style="color: green">// 10^x
</span>
    <span style="color: green">// 1 =&gt; 2: перебираем Set и заполняем вспомогательный SetTmp контейнерами с переставленными элементами
</span>    setEnumerator = set.getEnumerator();
    <span style="color: blue">while</span> (setEnumerator.moveNext())
    {
        [r1,d2,s3,s4,s5] = setEnumerator.current();
        <span style="color: green">// выводим на первые позиции контейнера все сортируемые по убыванию строковые поля - s3, s5
</span>        setTmp.add([s3,s5, r1,d2,s4]);
    }

    <span style="color: green">//  2 =&gt; 1: очищаем Set и заполняем его из SetTmp
</span>    <span style="color: green">// строками с добавленными в начало &quot;номерами&quot; для изменения порядка сортировки
</span>    set = <span style="color: blue">new</span> Set(Types::<span style="color: blue">Container</span>);
    i = tenInPwr;
    j = tenInPwr;
    s3prev = strMax(); 
    s5prev = strMax();
    setEnumerator = setTmp.getEnumerator();
    <span style="color: blue">while</span> (setEnumerator.moveNext())
    {
        [s3,s5, r1,d2,s4] = setEnumerator.current(); <span style="color: green">// например, s3 = &quot;Вася&quot;
</span>
        <span style="color: green">// первое убывающее текстовое поле
</span>        <span style="color: blue">if</span> (s3 != s3prev) <span style="color: green">// &quot;номер&quot; в s3 изменяем только при изменении значения
</span>        {
            i--;
            s3prev = s3;
        }
        s3 = subStr( int2str(tenInPwr+i), 2, pwr) + s3; <span style="color: green">// например, s3 станет = &quot;9999Вася&quot;
</span>
        <span style="color: green">// второе убывающее текстовое поле
</span>        <span style="color: blue">if</span> (s5 != s5prev) <span style="color: green">// &quot;номер&quot; в s5 изменяем только при изменении значения
</span>        {
            j--;
            s5prev = s5;
        }
        s5 = subStr( int2str(tenInPwr+j), 2, pwr) + s5;

        <span style="color: green">// здесь также можно преобразовать нетекстовые убывающие поля (если есть такие)
</span>
        <span style="color: green">// восстановление исходного порядка элементов в контейнере
</span>        set.add([r1,d2,s3,s4,s5]);
    }

    <span style="color: green">// окончательное извлечение и восстановление измененных значений
</span>    setEnumerator = set.getEnumerator();
    <span style="color: blue">while</span> (setEnumerator.moveNext())
    {
        [r1,d2,s3,s4,s5] = setEnumerator.current();
        s3 = subStr(s3, pwr+1, strLen(s3)); <span style="color: green">// &quot;9999Вася&quot; =&gt; &quot;Вася&quot;
</span>        s5 = subStr(s5, pwr+1, strLen(s5));
        <span style="color: green">// здесь также восстанавливаем нетекстовые убывающие поля (если есть)
</span>    }
<span style="color: green">//--------------------------------------------
</span>    info(strFmt(<span style="color: red">'Milliseconds: %1'</span>, WinAPI::getTickCount()-t));

    setEnumerator.reset();
    <span style="color: blue">while</span> (setEnumerator.moveNext())
    {
        [r1,d2,s3,s4,s5] = setEnumerator.current();
        s3 = subStr(s3, pwr+1, strLen(s3));
        s5 = subStr(s5, pwr+1, strLen(s5));

        info(strFmt(<span style="color: red">'%1 -- %2 -- %3 -- %4 -- %5'</span>, r1,d2,s3,s4,s5));
    }
}</pre></div>Если помимо обратной сортировки по текстовым полям требуется также и обратная сортировка по полям чисел или дат, то для них манипуляции существенно проще: их не надо переставлять внутри контейнера, достаточно лишь &quot;изменить знак&quot; перед окончательной (&quot;сортирующей&quot;) загрузкой в Set и затем в процессе считывания восстановить значения повторным &quot;изменением знака&quot;. Потенциальные места преобразования нетекстовых полей в джобе отмечены соответствующими комментариями.<br />
<br />
Используя сортирующую способность Set, не следует забывать и о его группирующей способности - содержать в себе только уникальные значения, игнорируя повторное добавление уже имеющихся элементов. Поэтому если сортируется набор неуникальных простых значений (чисел, дат, строк), их следует превратить в контейнеры путем добавления на вторую позицию уникального значения целочисленного счетчика (своеобразного &quot;RecId&quot;), как это было сделано, например, здесь: <a href="http://www.axforum.info/forums/showthread.php?p=152998" target="_blank">order by и group by</a> (в результате чего оба значения &quot;200&quot; попали в финальный результат). Если сортируются неуникальные контейнеры, то их так же надо сделать уникальными, добавив аналогичный &quot;RecId&quot; последним элементом.</div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=91</guid>
		</item>
		<item>
			<title>Приятное управление источниками данных ODBC</title>
			<link>//axforum.info/forums/blog.php?b=85</link>
			<pubDate>Fri, 19 Feb 2010 11:15:44 GMT</pubDate>
			<description><![CDATA[ВНИМАНИЕ! Материал ниже - не "узко"-аксаптовский, но "широко"-виндосовский! Поэтому "Добро пожаловать, ВСЕ!" :)

Приходилось ли вам скрежетать зубами при создании источника данных ODBC через стандартный интерфейс Windows: Старт \ Настройка \ Панель управления \ Администрирование \ Источники данных (ODBC) ? При создании одного источника, наверное, нет. А при создании пяти сразу? А при создании хоть и одного нового, но на десятках пользовательских компьютеров? Что и говорить, удовольствия от такого рутинного занятия не сильно много.

Изображение: http://www.axforum.info/forums/picture.php?albumid=9&pictureid=10 

Лет восемь примерно назад мне пришлось решать задачу по прописыванию ODBC-источника на компьютерах свыше полсотни пользователей. Тогда наш IT-отдел, состоящий из 3 человек, каждому из которых было лень оббегать пользователей по кругу и тупо повторять одно и то же десятки раз, совместными усилиями нарыл в Сети пример на VBA по автоматическому созданию источника. Мы подрихтовали код и упаковали его в mdb-файл Access'а как в командный файл, с автозапуском процедуры при открытии этого файла и последующим закрытием после срабатывания. Получившийся "командник" разослали пользователям по почте с просьбой запустить этот файл на своих машинах самостоятельно. Источники красиво создались, а мы, IT-шники, получили глубокое удовлетворение от содеянного.

Воды с тех пор утекло много. Участники того давнего действа теперь работают в других и разных местах. Код, как это часто бывает в таких случаях, даже если и сохранился где-то в архивах, то в них же и безнадежно затерялся и, возможно, лучшим выходом является просто повторное его создание заново. Так и пришлось сделать, когда мне недавно захотелось возродить забытую процедуру и уж на этот раз поместить его в легкодоступное место, коим я и считаю данное сообщение.

После развернутого лирического вступления далее покажу получившийся код (Excel VBA), намекну на возможности получившегося инструментика и приложу Excel-файл с необходимой начинкой, которым каждый из вас сможет при желании самостоятельно воспользоваться. Думаю, что то, что именно Excel является контейнером для решения этой задачи, для моих постоянных читателей не является неожиданностью ;)

Собственно, интерфейс "инструментика" - во всей его непосредственности - можно наблюдать уже на следующем рисунке - фрагменте рабочего листа Excel:

Изображение: http://www.axforum.info/forums/picture.php?albumid=9&pictureid=11 

На приведенном рисунке мы видим таблицу с наборами данных о DSN (область между серыми полосами; назовем ее мини-таблица) и небольшую голубую "панель управления" в нижней правой части. Состав колонок таблицы - достаточный для источников, предназначенных для работы с Oracle. При необходимости список колонок легко "расширяется" вправо путем добавления специфических столбцов для других драйверов ODBC. При этом соответствующие несложные изменения надо будет внести и в приведенный ниже код VBA. Само собой, что строки между серыми полосами можно добавлять и удалять, следя за тем, чтобы не было разрывов (пустых ячеек) в колонке "Driver" между строкой заголовков и последней заполненной строкой этой мини-таблицы.

Как всё это работает, думаю, понятно. Выбор опции в группе переключателей "DSN" эквивалентен выбору соответствующей вкладки на первом рисунке - User DSN или System DSN. Выбор во второй группе "Operation" имитирует одну из кнопок первого рисунка. Наконец по нажатию единственной кнопки "Do The Operation" выбранная операция для выбранного типа DSN выполняется в цикле для каждой строки (набора параметров) мини-таблицы.

Информация об успехе или неудаче операции для каждой строки выводится в окно отладки редактора Visual Basic. Вывод о результате делается на основании значения, возвращаемого функцией SQLConfigDataSource (см. в коде). Насколько я понимаю, изначально разработчиками этой функции предполагался возврат либо значения 1 (успех), либо 0 (неудача), что и проверяется в моей процедурке. Однако, в процессе тестирования драйверов для Oracle выяснилось, что для них возвращаются какие-то довольно большие целые числа - и в случае успеха, и в случае неудачи. Поэтому, если такой драйвер имеется в системе, то для него все операции, выполняемые с помощью моей процедуры считаются "успешными". Для таких драйверов убедиться в реальном положении вещей можно с помощью стандартной формы "ODBC Data Source Administrator".

Хотя код процедуры можно посмотреть в прилагаемом Excel-файле, продублирую его здесь - в основном, для гостей форума, не имеющих доступа к вложениям:
 

Код:
---------
Option Explicit

'--- Excel VBA ---

'Constant Declaration
'User DSN
Private Const ODBC_ADD_DSN = 1          ' Add data source
Private Const ODBC_CONFIG_DSN = 2       ' Configure (edit) data source
Private Const ODBC_REMOVE_DSN = 3       ' Remove data source

'System DSN
Private Const ODBC_ADD_SYS_DSN = 4      ' Add data source
Private Const ODBC_CONFIG_SYS_DSN = 5   ' Configure (edit) data source
Private Const ODBC_REMOVE_SYS_DSN = 6   ' Remove data source

Private Const vbAPINull As Long = 0     ' NULL Pointer

'Function Declare
#If Win32 Then
    Private Declare Function SQLConfigDataSource Lib "ODBCCP32.DLL" _
    (ByVal hwndParent As Long, ByVal fRequest As Long, _
    ByVal lpszDriver As String, ByVal lpszAttributes As String) _
    As Long
#Else
    Private Declare Function SQLConfigDataSource Lib "ODBCINST.DLL" _
    (ByVal hwndParent As Integer, ByVal fRequest As Integer, ByVal _
    lpszDriver As String, ByVal lpszAttributes As String) As Integer
#End If

Sub doSomethingWithDSNs()

    #If Win32 Then
        Dim intRet As Long
    #Else
        Dim intRet As Integer
    #End If
    
    Dim strDriver As String
    Dim strAttributes As String
    Dim rng As Range
    Dim i As Integer
    Dim fRequest As Long
    
    Select Case ThisWorkbook.Worksheets("List_of_DSNs").Range("DSNCell")
        Case 1
            Select Case ThisWorkbook.Worksheets("List_of_DSNs").Range("OperationCell")
                Case 1: fRequest = ODBC_ADD_DSN
                Case 2: fRequest = ODBC_REMOVE_DSN
                Case 3: fRequest = ODBC_CONFIG_DSN
            End Select
        Case 2
            Select Case ThisWorkbook.Worksheets("List_of_DSNs").Range("OperationCell")
                Case 1: fRequest = ODBC_ADD_SYS_DSN
                Case 2: fRequest = ODBC_REMOVE_SYS_DSN
                Case 3: fRequest = ODBC_CONFIG_SYS_DSN
            End Select
    End Select
    
    Set rng = ThisWorkbook.Worksheets("List_of_DSNs").Range("TableUpLeft").CurrentRegion
    
    For i = 2 To rng.Rows.Count
    
        strDriver = rng(i, 1)
        
        'Set the attributes delimited by null.
        'See driver documentation for a complete list of attributes.
        strAttributes = "DSN=" & rng(i, 2) & Chr(0)
        strAttributes = strAttributes & "DESCRIPTION=" & rng(i, 3) & Chr(0)
        strAttributes = strAttributes & "SERVER=" & rng(i, 4) & Chr(0)
        strAttributes = strAttributes & "UID=" & rng(i, 5) & Chr(0)
        strAttributes = strAttributes & "PWD=" & rng(i, 6) & Chr(0)
        
        'To show dialog, use Form1.Hwnd instead of vbAPINull.
        intRet = SQLConfigDataSource(vbAPINull, fRequest, strDriver, strAttributes)
        
        If intRet Then
            Debug.Print i - 1 & " -- Successful operation for " & rng(i, 2) & " with " & rng(i, 1)
        Else
            Debug.Print i - 1 & " -- Failed operation for " & rng(i, 2) & " with " & rng(i, 1)
        End If
        
    Next i
    
    MsgBox "You can see log in Immediate window (Alt+F11, Ctrl+G)", vbInformation, "FINISHED!"
            
End Sub
---------
Несколько слов об открывшихся возможностях, которые нравятся лично мне:
* Хранение информации о всех DSN в одном месте - табличке Excel, со всеми ее вытекающими прелестями типа дублирования значений копированием и т.п.
.
* Можно указать пароли. При помощи стандартного "Администратора" я не помню, чтобы такое было возможно. Обычно, когда это было нужно, я прописывал их напрямую в реестре Windows. А нужно это бывает, например, при открытии присоединенной таблички в Access - если пароль не прописан в реестре, то он раздражающе запрашивается; если же прописан, то легко открывается сразу как родная табличка Access. Плюс, в этом случае пароль совсем не обязательно сообщать конечному пользователю.
.
* Возможность автоматически выбрать нужный драйвер из нескольких возможных. В моем конкретном случае для Oracle это хорошо видно. У нас у некоторых пользователей уже установлены новые версии - "Oracle in VER10_HOME", в то время как у многих - еще старые "Oracle in OraHome92". Так вот, при прогоне процедуры ненужный драйвер просто не установится (если в системе имеются оба драйвера, то понятно, что установится первый встретившийся в мини-таблице).
.
* Конечно, эстетическая мелочь, но все равно приятно - можно неспешно указать "Description". Обычно это поле впопыхах админской жизни либо не заполняется вообще, либо туда дублируется строка из поля DSN. А уж о такой роскоши, как как-нибудь сесть за чашкой чая и привести в порядок эти описания и говорить не приходится - какой идиот захочет этим заниматься через "Administrator"?! А теперь - выставил всё в мини-табличке, запустил "Configure" - и вот оно, счастье!

В целом, мне кажется, вся рассмотренная выше задачка из серии тех, на которые при всей их простоте сам время найдёшь с трудом (и ведь будешь продолжать скрежетать зубами!), но если кто-то предлагает готовое решение, то почему бы не воспользоваться. В общем, приятного вам юзанья моей скромной тулзы!

"СПИСОК ЛИТЕРАТУРЫ":
1. http://forum.sources.ru/index.php?showtopic=141902&view=showall
2. http://support.microsoft.com/kb/171146/EN-US/
3. http://www.pcreview.co.uk/forums/thread-1632783.php
4. http://support.microsoft.com/kb/287668

(Ура! Почти на грани, но уложился в разрешенные 10 тыщ символов :))]]></description>
			<content:encoded><![CDATA[<div>ВНИМАНИЕ! Материал ниже - не &quot;узко&quot;-аксаптовский, но &quot;широко&quot;-виндосовский! Поэтому &quot;Добро пожаловать, ВСЕ!&quot; :)<br />
<br />
Приходилось ли вам скрежетать зубами при создании источника данных ODBC через стандартный интерфейс Windows: Старт \ Настройка \ Панель управления \ Администрирование \ Источники данных (ODBC) ? При создании одного источника, наверное, нет. А при создании пяти сразу? А при создании хоть и одного нового, но на десятках пользовательских компьютеров? Что и говорить, удовольствия от такого рутинного занятия не сильно много.<br />
<br />
<img src="http://www.axforum.info/forums/picture.php?albumid=9&amp;pictureid=10" border="0" alt="" /><br />
<br />
Лет восемь примерно назад мне пришлось решать задачу по прописыванию ODBC-источника на компьютерах свыше полсотни пользователей. Тогда наш IT-отдел, состоящий из 3 человек, каждому из которых было лень оббегать пользователей по кругу и тупо повторять одно и то же десятки раз, совместными усилиями нарыл в Сети пример на VBA по автоматическому созданию источника. Мы подрихтовали код и упаковали его в mdb-файл Access'а как в командный файл, с автозапуском процедуры при открытии этого файла и последующим закрытием после срабатывания. Получившийся &quot;командник&quot; разослали пользователям по почте с просьбой запустить этот файл на своих машинах самостоятельно. Источники красиво создались, а мы, IT-шники, получили глубокое удовлетворение от содеянного.<br />
<br />
Воды с тех пор утекло много. Участники того давнего действа теперь работают в других и разных местах. Код, как это часто бывает в таких случаях, даже если и сохранился где-то в архивах, то в них же и безнадежно затерялся и, возможно, лучшим выходом является просто повторное его создание заново. Так и пришлось сделать, когда мне недавно захотелось возродить забытую процедуру и уж на этот раз поместить его в легкодоступное место, коим я и считаю данное сообщение.<br />
<br />
После развернутого лирического вступления далее покажу получившийся код (Excel VBA), намекну на возможности получившегося инструментика и приложу Excel-файл с необходимой начинкой, которым каждый из вас сможет при желании самостоятельно воспользоваться. Думаю, что то, что именно Excel является контейнером для решения этой задачи, для моих постоянных читателей не является неожиданностью ;)<br />
<br />
Собственно, интерфейс &quot;инструментика&quot; - во всей его непосредственности - можно наблюдать уже на следующем рисунке - фрагменте рабочего листа Excel:<br />
<br />
<img src="http://www.axforum.info/forums/picture.php?albumid=9&amp;pictureid=11" border="0" alt="" /><br />
<br />
На приведенном рисунке мы видим таблицу с наборами данных о DSN (область между серыми полосами; назовем ее мини-таблица) и небольшую голубую &quot;панель управления&quot; в нижней правой части. Состав колонок таблицы - достаточный для источников, предназначенных для работы с Oracle. При необходимости список колонок легко &quot;расширяется&quot; вправо путем добавления специфических столбцов для других драйверов ODBC. При этом соответствующие несложные изменения надо будет внести и в приведенный ниже код VBA. Само собой, что строки между серыми полосами можно добавлять и удалять, следя за тем, чтобы не было разрывов (пустых ячеек) в колонке &quot;Driver&quot; между строкой заголовков и последней заполненной строкой этой мини-таблицы.<br />
<br />
Как всё это работает, думаю, понятно. Выбор опции в группе переключателей &quot;DSN&quot; эквивалентен выбору соответствующей вкладки на первом рисунке - User DSN или System DSN. Выбор во второй группе &quot;Operation&quot; имитирует одну из кнопок первого рисунка. Наконец по нажатию единственной кнопки &quot;Do The Operation&quot; выбранная операция для выбранного типа DSN выполняется в цикле для каждой строки (набора параметров) мини-таблицы.<br />
<br />
Информация об успехе или неудаче операции для каждой строки выводится в окно отладки редактора Visual Basic. Вывод о результате делается на основании значения, возвращаемого функцией SQLConfigDataSource (см. в коде). Насколько я понимаю, изначально разработчиками этой функции предполагался возврат либо значения 1 (успех), либо 0 (неудача), что и проверяется в моей процедурке. Однако, в процессе тестирования драйверов для Oracle выяснилось, что для них возвращаются какие-то довольно большие целые числа - и в случае успеха, и в случае неудачи. Поэтому, если такой драйвер имеется в системе, то для него все операции, выполняемые с помощью моей процедуры считаются &quot;успешными&quot;. Для таких драйверов убедиться в реальном положении вещей можно с помощью стандартной формы &quot;ODBC Data Source Administrator&quot;.<br />
<br />
Хотя код процедуры можно посмотреть в прилагаемом Excel-файле, продублирую его здесь - в основном, для гостей форума, не имеющих доступа к вложениям:<br />
 <br />
<div class="xpp"><div class="smallfont xpp_title">Код:</div><pre class="alt2 xpp_code">Option Explicit

'--- Excel VBA ---

'Constant Declaration
'User DSN
Private Const ODBC_ADD_DSN = 1          ' Add data source
Private Const ODBC_CONFIG_DSN = 2       ' Configure (edit) data source
Private Const ODBC_REMOVE_DSN = 3       ' Remove data source

'System DSN
Private Const ODBC_ADD_SYS_DSN = 4      ' Add data source
Private Const ODBC_CONFIG_SYS_DSN = 5   ' Configure (edit) data source
Private Const ODBC_REMOVE_SYS_DSN = 6   ' Remove data source

Private Const vbAPINull As Long = 0     ' NULL Pointer

'Function Declare
#If Win32 Then
    Private Declare Function SQLConfigDataSource Lib &quot;ODBCCP32.DLL&quot; _
    (ByVal hwndParent As Long, ByVal fRequest As Long, _
    ByVal lpszDriver As String, ByVal lpszAttributes As String) _
    As Long
#Else
    Private Declare Function SQLConfigDataSource Lib &quot;ODBCINST.DLL&quot; _
    (ByVal hwndParent As Integer, ByVal fRequest As Integer, ByVal _
    lpszDriver As String, ByVal lpszAttributes As String) As Integer
#End If

Sub doSomethingWithDSNs()

    #If Win32 Then
        Dim intRet As Long
    #Else
        Dim intRet As Integer
    #End If
    
    Dim strDriver As String
    Dim strAttributes As String
    Dim rng As Range
    Dim i As Integer
    Dim fRequest As Long
    
    Select Case ThisWorkbook.Worksheets(&quot;List_of_DSNs&quot;).Range(&quot;DSNCell&quot;)
        Case 1
            Select Case ThisWorkbook.Worksheets(&quot;List_of_DSNs&quot;).Range(&quot;OperationCell&quot;)
                Case 1: fRequest = ODBC_ADD_DSN
                Case 2: fRequest = ODBC_REMOVE_DSN
                Case 3: fRequest = ODBC_CONFIG_DSN
            End Select
        Case 2
            Select Case ThisWorkbook.Worksheets(&quot;List_of_DSNs&quot;).Range(&quot;OperationCell&quot;)
                Case 1: fRequest = ODBC_ADD_SYS_DSN
                Case 2: fRequest = ODBC_REMOVE_SYS_DSN
                Case 3: fRequest = ODBC_CONFIG_SYS_DSN
            End Select
    End Select
    
    Set rng = ThisWorkbook.Worksheets(&quot;List_of_DSNs&quot;).Range(&quot;TableUpLeft&quot;).CurrentRegion
    
    For i = 2 To rng.Rows.Count
    
        strDriver = rng(i, 1)
        
        'Set the attributes delimited by null.
        'See driver documentation for a complete list of attributes.
        strAttributes = &quot;DSN=&quot; &amp; rng(i, 2) &amp; Chr(0)
        strAttributes = strAttributes &amp; &quot;DESCRIPTION=&quot; &amp; rng(i, 3) &amp; Chr(0)
        strAttributes = strAttributes &amp; &quot;SERVER=&quot; &amp; rng(i, 4) &amp; Chr(0)
        strAttributes = strAttributes &amp; &quot;UID=&quot; &amp; rng(i, 5) &amp; Chr(0)
        strAttributes = strAttributes &amp; &quot;PWD=&quot; &amp; rng(i, 6) &amp; Chr(0)
        
        'To show dialog, use Form1.Hwnd instead of vbAPINull.
        intRet = SQLConfigDataSource(vbAPINull, fRequest, strDriver, strAttributes)
        
        If intRet Then
            Debug.Print i - 1 &amp; &quot; -- Successful operation for &quot; &amp; rng(i, 2) &amp; &quot; with &quot; &amp; rng(i, 1)
        Else
            Debug.Print i - 1 &amp; &quot; -- Failed operation for &quot; &amp; rng(i, 2) &amp; &quot; with &quot; &amp; rng(i, 1)
        End If
        
    Next i
    
    MsgBox &quot;You can see log in Immediate window (Alt+F11, Ctrl+G)&quot;, vbInformation, &quot;FINISHED!&quot;
            
End Sub</pre></div>Несколько слов об открывшихся возможностях, которые нравятся лично мне:<ul><li>Хранение информации о всех DSN в одном месте - табличке Excel, со всеми ее вытекающими прелестями типа дублирования значений копированием и т.п.<br />
.</li>
<li>Можно указать пароли. При помощи стандартного &quot;Администратора&quot; я не помню, чтобы такое было возможно. Обычно, когда это было нужно, я прописывал их напрямую в реестре Windows. А нужно это бывает, например, при открытии присоединенной таблички в Access - если пароль не прописан в реестре, то он раздражающе запрашивается; если же прописан, то легко открывается сразу как родная табличка Access. Плюс, в этом случае пароль совсем не обязательно сообщать конечному пользователю.<br />
.</li>
<li>Возможность автоматически выбрать нужный драйвер из нескольких возможных. В моем конкретном случае для Oracle это хорошо видно. У нас у некоторых пользователей уже установлены новые версии - &quot;Oracle in VER10_HOME&quot;, в то время как у многих - еще старые &quot;Oracle in OraHome92&quot;. Так вот, при прогоне процедуры ненужный драйвер просто не установится (если в системе имеются оба драйвера, то понятно, что установится первый встретившийся в мини-таблице).<br />
.</li>
<li>Конечно, эстетическая мелочь, но все равно приятно - можно неспешно указать &quot;Description&quot;. Обычно это поле впопыхах админской жизни либо не заполняется вообще, либо туда дублируется строка из поля DSN. А уж о такой роскоши, как как-нибудь сесть за чашкой чая и привести в порядок эти описания и говорить не приходится - какой идиот захочет этим заниматься через &quot;Administrator&quot;?! А теперь - выставил всё в мини-табличке, запустил &quot;Configure&quot; - и вот оно, счастье!</li>
</ul>В целом, мне кажется, вся рассмотренная выше задачка из серии тех, на которые при всей их простоте сам время найдёшь с трудом (и ведь будешь продолжать скрежетать зубами!), но если кто-то предлагает готовое решение, то почему бы не воспользоваться. В общем, приятного вам юзанья моей скромной тулзы!<br />
<br />
&quot;СПИСОК ЛИТЕРАТУРЫ&quot;:<ol style="list-style-type: decimal"><li><a href="http://forum.sources.ru/index.php?showtopic=141902&amp;view=showall" target="_blank">http://forum.sources.ru/index.php?sh...2&amp;view=showall</a></li>
<li><a href="http://support.microsoft.com/kb/171146/EN-US/" target="_blank">http://support.microsoft.com/kb/171146/EN-US/</a></li>
<li><a href="http://www.pcreview.co.uk/forums/thread-1632783.php" target="_blank">http://www.pcreview.co.uk/forums/thread-1632783.php</a></li>
<li><a href="http://support.microsoft.com/kb/287668" target="_blank">http://support.microsoft.com/kb/287668</a></li>
</ol>(Ура! Почти на грани, но уложился в разрешенные 10 тыщ символов :))</div>


<!-- attachments -->
	<div style="margin-top:10px">

		
		
		
		
			<fieldset class="fieldset">
				<legend>Вложения</legend>
				<table cellpadding="0" cellspacing="3" border="0">
				<tr>
	<td><img class="inlineimg" src="http://axforum.info//img.axforum.info/attach/xls.gif" alt="Тип файла: xls" width="16" height="16" border="0" style="vertical-align:baseline" /></td>
	<td><a href="//axforum.info/forums/blog_attachment.php?attachmentid=45&amp;d=1266576846">ComfortODBC.xls</a> (51.0 Кб, 3415 просмотров)</td>
</tr>
				</table>
			</fieldset>
		

	</div>
<!-- / attachments -->
]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=85</guid>
		</item>
		<item>
			<title>Цикл по сквозным годомесяцам</title>
			<link>//axforum.info/forums/blog.php?b=31</link>
			<pubDate>Wed, 20 Jan 2010 10:00:54 GMT</pubDate>
			<description><![CDATA[Эта "новая" единица измерения времени - "годомесяц" = год*12+месяц - всем наверняка знакома. Сама по себе в абсолютном значении величина эта смысла не имеет, интересны лишь ее изменения в некотором диапазоне. Любопытно, что я поискал в репозитарии по строке "mthofyr" и в явном виде подобный цикл ни разу не увидел, хотя разницы "годомесяцев" местами используются, например, в функции GM_Global::mthDiff в приложении GMCS.

Мне этот цикл пригодился в задаче нахождения расхождений между записями LedgerTrans в одной компании и соответствующими (по трансляции) записями в другой компании. Поскольку записей за без квартала три года имелось уже очень приличное количество (несколько миллионов), а для выявления расхождений использовался метод difference класса Set, то чтобы не делать сравниваемые множества слишком большими, было решено идти по месяцам - на каждом шаге определялись даты начала и конца месяца, которые затем подставлялись в условие where (в джобе ниже - currFirstDay и currLastDay соответственно).

Получившийся шаблончик цикла мне понравился и я решил его зафиксировать, дабы при случае еще раз воспользоваться и более не копаться в календарных функциях. Шаблон эффективен только в том случае, если цикл затрагивает более одного года (внутри одного и того же года, понятное дело - всё гораздо проще). 

Вот как выглядит этот помесячный цикл:

static void jobYearMonthLoop(Args _args)
{
    TransDate   dateBeg = 01\10\2006;
    TransDate   dateEnd = 30\06\2009;
    TransDate   currFirstDay, currLastDay;
    int         m, currYear, currMonth;
    ;
    for (m = year(dateBeg)*12+mthofyr(dateBeg);
         m<= year(dateEnd)*12+mthofyr(dateEnd);
         m++)
    {
        currMonth = m mod 12 ? m mod 12 : 12;
        currYear  = (m-currMonth) / 12;

        currFirstDay = mkDate(1,currMonth,currYear);
        currLastDay  = dateEndMth(currFirstDay);
        info(strFmt('%1 -- %2', currFirstDay, currLastDay));
    }
}
Ну, и до кучи - сама собой напрашивающаяся версия цикла для "годокварталов":

static void jobYearQuarterLoop(Args _args)
{
    TransDate   dateBeg = 01\10\2006;
    TransDate   dateEnd = 30\06\2009;
    TransDate   currFirstDay, currLastDay;
    int         q, currYear, currQtr;
    ;
    for (q = year(dateBeg)*4+date2qtr(dateBeg);
         q<= year(dateEnd)*4+date2qtr(dateEnd); 
         q++)
    {
        currQtr = q mod 4 ? q mod 4 : 4;
        currYear  = (q-currQtr) / 4;

        currFirstDay = mkDate(1,currQtr*3-2,currYear);
        currLastDay  = dateEndQtr(currFirstDay);
        info(strFmt('%1 -- %2', currFirstDay, currLastDay));
    }
}
]]></description>
			<content:encoded><![CDATA[<div>Эта &quot;новая&quot; единица измерения времени - &quot;годомесяц&quot; = год*12+месяц - всем наверняка знакома. Сама по себе в абсолютном значении величина эта смысла не имеет, интересны лишь ее изменения в некотором диапазоне. Любопытно, что я поискал в репозитарии по строке &quot;mthofyr&quot; и в явном виде подобный цикл ни разу не увидел, хотя разницы &quot;годомесяцев&quot; местами используются, например, в функции GM_Global::mthDiff в приложении GMCS.<br />
<br />
Мне этот цикл пригодился в задаче нахождения расхождений между записями LedgerTrans в одной компании и соответствующими (по трансляции) записями в другой компании. Поскольку записей за без квартала три года имелось уже очень приличное количество (несколько миллионов), а для выявления расхождений использовался метод difference класса Set, то чтобы не делать сравниваемые множества слишком большими, было решено идти по месяцам - на каждом шаге определялись даты начала и конца месяца, которые затем подставлялись в условие where (в джобе ниже - currFirstDay и currLastDay соответственно).<br />
<br />
Получившийся шаблончик цикла мне понравился и я решил его зафиксировать, дабы при случае еще раз воспользоваться и более не копаться в календарных функциях. Шаблон эффективен только в том случае, если цикл затрагивает более одного года (внутри одного и того же года, понятное дело - всё гораздо проще). <br />
<br />
Вот как выглядит этот помесячный цикл:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">static</span> <span style="color: blue">void</span> jobYearMonthLoop(Args _args)
{
    TransDate   dateBeg = 01\10\2006;
    TransDate   dateEnd = 30\06\2009;
    TransDate   currFirstDay, currLastDay;
    <span style="color: blue">int</span>         m, currYear, currMonth;
    ;
    <span style="color: blue">for</span> (m = year(dateBeg)*12+mthofyr(dateBeg);
         m&lt;= year(dateEnd)*12+mthofyr(dateEnd);
         m++)
    {
        currMonth = m <span style="color: blue">mod</span> 12 ? m <span style="color: blue">mod</span> 12 : 12;
        currYear  = (m-currMonth) / 12;

        currFirstDay = mkDate(1,currMonth,currYear);
        currLastDay  = dateEndMth(currFirstDay);
        info(strFmt(<span style="color: red">'%1 -- %2'</span>, currFirstDay, currLastDay));
    }
}</pre></div>Ну, и до кучи - сама собой напрашивающаяся версия цикла для &quot;годокварталов&quot;:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">static</span> <span style="color: blue">void</span> jobYearQuarterLoop(Args _args)
{
    TransDate   dateBeg = 01\10\2006;
    TransDate   dateEnd = 30\06\2009;
    TransDate   currFirstDay, currLastDay;
    <span style="color: blue">int</span>         q, currYear, currQtr;
    ;
    <span style="color: blue">for</span> (q = year(dateBeg)*4+date2qtr(dateBeg);
         q&lt;= year(dateEnd)*4+date2qtr(dateEnd); 
         q++)
    {
        currQtr = q <span style="color: blue">mod</span> 4 ? q <span style="color: blue">mod</span> 4 : 4;
        currYear  = (q-currQtr) / 4;

        currFirstDay = mkDate(1,currQtr*3-2,currYear);
        currLastDay  = dateEndQtr(currFirstDay);
        info(strFmt(<span style="color: red">'%1 -- %2'</span>, currFirstDay, currLastDay));
    }
}</pre></div></div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=31</guid>
		</item>
		<item>
			<title><![CDATA[Вывод в Excel сводной таблицы "пользователи-группы"]]></title>
			<link>//axforum.info/forums/blog.php?b=60</link>
			<pubDate>Mon, 21 Dec 2009 11:32:00 GMT</pubDate>
			<description><![CDATA[В принципе задача не бог весть какая сложная, хотя и сама по себе достаточно полезная с точки зрения администрирования Аксапты. Я, наверное, и не стал бы постить ее решение, если бы не маленькая изюминка - исходные данные из Аксапты выводятся не на лист Excel, а сразу в кэш сводной таблицы. Точнее, кэшу передается ADODB.Recordset, предварительно сформированный в памяти в Аксапте. Поэтому сообщение можно считать приложением к, не побоюсь этого слова, ставшей популярной теме Поговорим об ADO (http://axforum.info/forums/showthread.php?t=12973) :) 

Вот джоб, строящий в Excel перекрестную таблицу со строками-пользователями и столбцами-группами:

#CCADO
#define.xlExternal(2)
#define.xlRowField(1)
#define.xlColumnField(2)

static void Job_showUserGroupInExcelPivot(Args _args)
{
    UserGroupList   userGroupList;
    UserInfo        userInfo;
    UserGroupInfo   userGroupInfo;

    COM             rst, flds, fld;
    COM             wbk, pc, ptb, pf;
    COM             rng = SysExcelApplication::construct().workbooks().add().worksheets().itemFromNum(1).range('A1').comObject();
    ;

    rst = AdoRst::openRecordsetInMemory([
            ['UserId'    , #adVarChar,  5 ],
            ['UserName'  , #adVarChar, 40 ],
            ['GroupId'   , #adVarChar, 10 ],
            ['GroupName' , #adVarChar, 40 ]]);

    flds = rst.Fields();

    while select userGroupList
        join userInfo
            where userInfo.id == userGroupList.userId
        join userGroupInfo
            where userGroupInfo.id == userGroupList.groupId
    {
        rst.AddNew();
            fld = flds.Item('UserId'   ); fld.Value(userGroupList.userId );
            fld = flds.Item('UserName' ); fld.Value(userInfo.name        );
            fld = flds.Item('GroupId'  ); fld.Value(userGroupList.groupId);
            fld = flds.Item('GroupName'); fld.Value(userGroupInfo.name   );
        rst.Update();
    }

    wbk = rng.Parent(); wbk = wbk.Parent(); // ActiveWorkbook

    pc = wbk.PivotCaches(); pc = pc.Add(#xlExternal); // PivotCache
    pc.Recordset(rst); // вот здесь и передаем рекордсет -> кэшу

    ptb = pc.CreatePivotTable(rng); // PivotTable

    pf = ptb.PivotFields('UserName' ); pf.Orientation(#xlRowField   ); pf.Position(1); pf.Subtotals(1,false);
    pf = ptb.PivotFields('UserId'   ); pf.Orientation(#xlRowField   ); pf.Position(2); pf.Subtotals(1,false);

    pf = ptb.PivotFields('GroupId'  ); pf.Orientation(#xlColumnField); pf.Position(1); pf.Subtotals(1,false);
    pf = ptb.PivotFields('GroupName'); // PivotField
    ptb.AddDataField(pf, 'Участие пользователей в группах');
                                       pf.Orientation(#xlColumnField); pf.Position(2); pf.Subtotals(1,false);

    COM::createFromObject(rng.Application()).Visible(true);
}
Как видно, код джоба получился вполне компактным - в первую очередь, за счет того, что везде, где можно, используются свойства сводной таблицы по умолчанию. Признаком вхождения конкретного пользователя в конкретную группу является результат (равный 1) агрегатной функции Count по последнему полю GroupName. Причем, нюанс - сначала это поле помещается в область данных сводной таблицы и лишь заключительным шагом - в область заголовков строк. Использовавшееся вначале решение "в лоб", когда поле первым делом помещалось в область заголовков строк и затем в область данных, приводило к тому, что поле исчезало из заголовков строк. Ну, а почему выбрано именно последнее поле, думаю, понятно - чтобы не делать лишнего присваивания "pf = ...".

Рекомендую также обратить внимание на способ выключения ненужных подитогов для полей при помощи конструкции "Subtotals(1,false)". При записи процедуры макрорекордером в Excel на этом шаге прописывается вариантный массив VBA вида Array(False,False,False...), имитация которого в джобе при помощи COMVariant.safeArray увеличила бы размер его кода раза в два - за счёт подготовительных действий по созданию массива.

Используемый в джобе для построения рекордсета в памяти статический метод openRecordsetInMemory - член некоторого моего класса AdoRst. Метод можно поместить и в Global, либо развернуть как вложенную функцию в самом джобе. Единственный параметр метода - контейнер контейнеров-полей.

#CCADO
static COM openRecordsetInMemory(container _fields)
{
    COM rst  = new COM('ADODB.Recordset');
    COM flds = rst.Fields();

    int i;
    str fldName;
    int fldType;
    int fldDefinedSize;
    ;

    for (i=1;i<=conLen(_fields);i++)
    {
        [fldName, fldType, fldDefinedSize] = conPeek(_fields, i);

        if (! fldType)
            fldType = #adVarChar;

        if (! fldDefinedSize            &&
           (fldType == #adChar          || // 129
            fldType == #adWChar         )) // 130
        {
            fldDefinedSize = 30;
        }

        if (! fldDefinedSize            &&
           (fldType == #adVarChar       || // 200
            fldType == #adLongVarChar   || // 201
            fldType == #adVarWChar      || // 202
            fldType == #adLongVarWChar  )) // 203
        {
            fldDefinedSize = 255;
        }

        if (fldDefinedSize)
            flds.Append(fldName, fldType, fldDefinedSize);
        else
            flds.Append(fldName, fldType);
    }

    rst.Open();

    return rst;
}
К великому моему сожалению, данный метод передачи рекордсета сводной таблице нельзя применить в случае ActiveX - OWC PivotTable. В его объектной модели, увы, отсутствует объект PivotCache...]]></description>
			<content:encoded><![CDATA[<div>В принципе задача не бог весть какая сложная, хотя и сама по себе достаточно полезная с точки зрения администрирования Аксапты. Я, наверное, и не стал бы постить ее решение, если бы не маленькая изюминка - исходные данные из Аксапты выводятся не на лист Excel, а сразу в кэш сводной таблицы. Точнее, кэшу передается ADODB.Recordset, предварительно сформированный в памяти в Аксапте. Поэтому сообщение можно считать приложением к, не побоюсь этого слова, ставшей популярной теме <a href="http://axforum.info/forums/showthread.php?t=12973" target="_blank">Поговорим об ADO</a> :) <br />
<br />
Вот джоб, строящий в Excel перекрестную таблицу со строками-пользователями и столбцами-группами:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">#CCADO
#define.xlExternal(2)
#define.xlRowField(1)
#define.xlColumnField(2)

<span style="color: blue">static</span> <span style="color: blue">void</span> Job_showUserGroupInExcelPivot(Args _args)
{
    UserGroupList   userGroupList;
    UserInfo        userInfo;
    UserGroupInfo   userGroupInfo;

    COM             rst, flds, fld;
    COM             wbk, pc, ptb, pf;
    COM             rng = SysExcelApplication::construct().workbooks().add().worksheets().itemFromNum(1).range(<span style="color: red">'A1'</span>).comObject();
    ;

    rst = AdoRst::openRecordsetInMemory([
            [<span style="color: red">'UserId'</span>    , #adVarChar,  5 ],
            [<span style="color: red">'UserName'</span>  , #adVarChar, 40 ],
            [<span style="color: red">'GroupId'</span>   , #adVarChar, 10 ],
            [<span style="color: red">'GroupName'</span> , #adVarChar, 40 ]]);

    flds = rst.Fields();

    <span style="color: blue">while</span> <span style="color: blue">select</span> userGroupList
        <span style="color: blue">join</span> userInfo
            <span style="color: blue">where</span> userInfo.id == userGroupList.userId
        <span style="color: blue">join</span> userGroupInfo
            <span style="color: blue">where</span> userGroupInfo.id == userGroupList.groupId
    {
        rst.AddNew();
            fld = flds.Item(<span style="color: red">'UserId'</span>   ); fld.Value(userGroupList.userId );
            fld = flds.Item(<span style="color: red">'UserName'</span> ); fld.Value(userInfo.name        );
            fld = flds.Item(<span style="color: red">'GroupId'</span>  ); fld.Value(userGroupList.groupId);
            fld = flds.Item(<span style="color: red">'GroupName'</span>); fld.Value(userGroupInfo.name   );
        rst.Update();
    }

    wbk = rng.Parent(); wbk = wbk.Parent(); <span style="color: green">// ActiveWorkbook
</span>
    pc = wbk.PivotCaches(); pc = pc.Add(#xlExternal); <span style="color: green">// PivotCache
</span>    pc.Recordset(rst); <span style="color: green">// вот здесь и передаем рекордсет -&gt; кэшу
</span>
    ptb = pc.CreatePivotTable(rng); <span style="color: green">// PivotTable
</span>
    pf = ptb.PivotFields(<span style="color: red">'UserName'</span> ); pf.Orientation(#xlRowField   ); pf.Position(1); pf.Subtotals(1,<span style="color: blue">false</span>);
    pf = ptb.PivotFields(<span style="color: red">'UserId'</span>   ); pf.Orientation(#xlRowField   ); pf.Position(2); pf.Subtotals(1,<span style="color: blue">false</span>);

    pf = ptb.PivotFields(<span style="color: red">'GroupId'</span>  ); pf.Orientation(#xlColumnField); pf.Position(1); pf.Subtotals(1,<span style="color: blue">false</span>);
    pf = ptb.PivotFields(<span style="color: red">'GroupName'</span>); <span style="color: green">// PivotField
</span>    ptb.AddDataField(pf, <span style="color: red">'Участие пользователей в группах'</span>);
                                       pf.Orientation(#xlColumnField); pf.Position(2); pf.Subtotals(1,<span style="color: blue">false</span>);

    COM::createFromObject(rng.Application()).Visible(<span style="color: blue">true</span>);
}</pre></div>Как видно, код джоба получился вполне компактным - в первую очередь, за счет того, что везде, где можно, используются свойства сводной таблицы по умолчанию. Признаком вхождения конкретного пользователя в конкретную группу является результат (равный 1) агрегатной функции Count по последнему полю GroupName. Причем, нюанс - сначала это поле помещается в область данных сводной таблицы и лишь заключительным шагом - в область заголовков строк. Использовавшееся вначале решение &quot;в лоб&quot;, когда поле первым делом помещалось в область заголовков строк и затем в область данных, приводило к тому, что поле исчезало из заголовков строк. Ну, а почему выбрано именно последнее поле, думаю, понятно - чтобы не делать лишнего присваивания &quot;pf = ...&quot;.<br />
<br />
Рекомендую также обратить внимание на способ выключения ненужных подитогов для полей при помощи конструкции &quot;Subtotals(1,false)&quot;. При записи процедуры макрорекордером в Excel на этом шаге прописывается вариантный массив VBA вида Array(False,False,False...), имитация которого в джобе при помощи COMVariant.safeArray увеличила бы размер его кода раза в два - за счёт подготовительных действий по созданию массива.<br />
<br />
Используемый в джобе для построения рекордсета в памяти статический метод openRecordsetInMemory - член некоторого моего класса AdoRst. Метод можно поместить и в Global, либо развернуть как вложенную функцию в самом джобе. Единственный параметр метода - контейнер контейнеров-полей.<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">#CCADO
<span style="color: blue">static</span> COM openRecordsetInMemory(<span style="color: blue">container</span> _fields)
{
    COM rst  = <span style="color: blue">new</span> COM(<span style="color: red">'ADODB.Recordset'</span>);
    COM flds = rst.Fields();

    <span style="color: blue">int</span> i;
    <span style="color: blue">str</span> fldName;
    <span style="color: blue">int</span> fldType;
    <span style="color: blue">int</span> fldDefinedSize;
    ;

    <span style="color: blue">for</span> (i=1;i&lt;=conLen(_fields);i++)
    {
        [fldName, fldType, fldDefinedSize] = conPeek(_fields, i);

        <span style="color: blue">if</span> (! fldType)
            fldType = #adVarChar;

        <span style="color: blue">if</span> (! fldDefinedSize            &amp;&amp;
           (fldType == #adChar          || <span style="color: green">// 129
</span>            fldType == #adWChar         )) <span style="color: green">// 130
</span>        {
            fldDefinedSize = 30;
        }

        <span style="color: blue">if</span> (! fldDefinedSize            &amp;&amp;
           (fldType == #adVarChar       || <span style="color: green">// 200
</span>            fldType == #adLongVarChar   || <span style="color: green">// 201
</span>            fldType == #adVarWChar      || <span style="color: green">// 202
</span>            fldType == #adLongVarWChar  )) <span style="color: green">// 203
</span>        {
            fldDefinedSize = 255;
        }

        <span style="color: blue">if</span> (fldDefinedSize)
            flds.Append(fldName, fldType, fldDefinedSize);
        <span style="color: blue">else</span>
            flds.Append(fldName, fldType);
    }

    rst.Open();

    <span style="color: blue">return</span> rst;
}</pre></div>К великому моему сожалению, данный метод передачи рекордсета сводной таблице нельзя применить в случае ActiveX - OWC PivotTable. В его объектной модели, увы, отсутствует объект PivotCache...</div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=60</guid>
		</item>
		<item>
			<title>События FormActiveXControl не получается использовать в диалоге</title>
			<link>//axforum.info/forums/blog.php?b=50</link>
			<pubDate>Tue, 17 Nov 2009 15:49:52 GMT</pubDate>
			<description><![CDATA[Хотел воспользоваться такими родными событиями Spreadsheet в динамической форме (диалоге), как SelectionChange, DblClick и еще некоторыми, которые видны через ActiveX Explorer при правом щелчке на узле контрола этого типа в дизайне обычной формы, сохраненной в AOT. 

Уверенности придавал тот факт, что в форме, сохраненной в AOT, всё работает отлично. Методы событий добавляются во время разработки при помощи ActiveX Explorer с именами onEvent_SelectionChange, onEvent_DblClick и т.п. Внедренный в форму Spreadsheet (как FormActiveXControl) прекрасно на них реагирует и должным образом отрабатывает.

Как известно, для обработки событий в диалоге приходится прибегать к хитростям "перегрузки", заключающимся в использовании операторов

<FormRun>.controlMethodOverload(true);

<FormRun>.controlMethodOverloadObject
 ( new <класс обработки событий данного диалога>(<FormRun>) );
в некотором методе, формирующем диалог перед его исполнением.

Во втором операторе необходимо указать место хранения методов обработки событий. Таким местом обычно выступает некий отдельно стоящий класс, используемый только для данного диалога (в сам диалог по понятным "динамическим" причинам мы, к сожалению, не можем добавить методы событий).

Также известно, что для названий методов событий в таком классе существуют определенные правила. Согласно этим правилам название метода должно начинаться с имени элемента управления ("контрола") в форме и  и далее через символ подчеркивания - название выбранного метода. Если в форме имеется кнопка MyButton, нажатие на которую обрабатывается методом clicked(), то предназначенный для тех же целей метод, вынесенный в класс, должен называться MyButton_clicked().

Имея в диалоге Spreadsheet с именем ss, я создал поддерживающий класс с методом под названием ss_onEvent_SelectionChange(). В метод я поместил оператор box::info('Изменилась ячейка!') и стал щелкать мышкой по разным ячейкам Spreadsheet'а. Ожидаемая с каждым щелчком реакция - появление сообщения - не наблюдалась.

Сделал шаг назад - вернулся к сохраненной в AOT обычной форме, но обработку событий оставил в поддерживающем классе (вызов element.controlMethodOverload поместил в init формы). И опять Spreasheet никак не реагировал :( Родная же аксаптовская кнопка (FormButtonContol) нормально "кликалась" со срабатыванием вынесенного метода MyButton_clicked() во всех вариантах: и в диалоге, и в сохраненной форме.

И всё же для этого промежуточного варианта - сохраненная форма, но вынесенная обработка - решение нашлось. Оказалось, что для вынесенного метода в узле методов Spreadsheet'а сохраненной формы должен присутствовать аналогичный пустой метод. Например, чтобы сработал вынесенный метод ss_onEvent_SelectionChange() - в узле ss\Methods сохраненной формы должен присутствовать метод с кодом "void onEvent_SelectionChange(){}", добавленный с помощью ActiveX Explorer. 

И хотя практического смысла в таком промежуточном решении немного, важно другое - получен ответ, пусть и отрицательный, на вопрос "Можно ли использовать события для FormActiveXControl в диалоге?". Нельзя! Негде для диалога написать "void onEvent_SelectionChange(){}"... (Ax 3.0 SP4)

Или всё-таки можно? ;) Может, как-то в более новых версиях?..]]></description>
			<content:encoded><![CDATA[<div>Хотел воспользоваться такими родными событиями Spreadsheet в динамической форме (диалоге), как SelectionChange, DblClick и еще некоторыми, которые видны через ActiveX Explorer при правом щелчке на узле контрола этого типа в дизайне обычной формы, сохраненной в AOT. <br />
<br />
Уверенности придавал тот факт, что в форме, сохраненной в AOT, всё работает отлично. Методы событий добавляются во время разработки при помощи ActiveX Explorer с именами onEvent_SelectionChange, onEvent_DblClick и т.п. Внедренный в форму Spreadsheet (как FormActiveXControl) прекрасно на них реагирует и должным образом отрабатывает.<br />
<br />
Как известно, для обработки событий в диалоге приходится прибегать к хитростям &quot;перегрузки&quot;, заключающимся в использовании операторов<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">&lt;FormRun&gt;.controlMethodOverload(<span style="color: blue">true</span>);

&lt;FormRun&gt;.controlMethodOverloadObject
 ( <span style="color: blue">new</span> &lt;    &gt;(&lt;FormRun&gt;) );</pre></div>в некотором методе, формирующем диалог перед его исполнением.<br />
<br />
Во втором операторе необходимо указать место хранения методов обработки событий. Таким местом обычно выступает некий отдельно стоящий класс, используемый только для данного диалога (в сам диалог по понятным &quot;динамическим&quot; причинам мы, к сожалению, не можем добавить методы событий).<br />
<br />
Также известно, что для названий методов событий в таком классе существуют определенные правила. Согласно этим правилам название метода должно начинаться с имени элемента управления (&quot;контрола&quot;) в форме и  и далее через символ подчеркивания - название выбранного метода. Если в форме имеется кнопка MyButton, нажатие на которую обрабатывается методом clicked(), то предназначенный для тех же целей метод, вынесенный в класс, должен называться MyButton_clicked().<br />
<br />
Имея в диалоге Spreadsheet с именем ss, я создал поддерживающий класс с методом под названием ss_onEvent_SelectionChange(). В метод я поместил оператор box::info('Изменилась ячейка!') и стал щелкать мышкой по разным ячейкам Spreadsheet'а. Ожидаемая с каждым щелчком реакция - появление сообщения - не наблюдалась.<br />
<br />
Сделал шаг назад - вернулся к сохраненной в AOT обычной форме, но обработку событий оставил в поддерживающем классе (вызов element.controlMethodOverload поместил в init формы). И опять Spreasheet никак не реагировал :( Родная же аксаптовская кнопка (FormButtonContol) нормально &quot;кликалась&quot; со срабатыванием вынесенного метода MyButton_clicked() во всех вариантах: и в диалоге, и в сохраненной форме.<br />
<br />
И всё же для этого промежуточного варианта - сохраненная форма, но вынесенная обработка - решение нашлось. Оказалось, что для вынесенного метода в узле методов Spreadsheet'а сохраненной формы должен присутствовать аналогичный пустой метод. Например, чтобы сработал вынесенный метод ss_onEvent_SelectionChange() - в узле ss\Methods сохраненной формы должен присутствовать метод с кодом &quot;void onEvent_SelectionChange(){}&quot;, добавленный с помощью ActiveX Explorer. <br />
<br />
И хотя практического смысла в таком промежуточном решении немного, важно другое - получен ответ, пусть и отрицательный, на вопрос &quot;Можно ли использовать события для FormActiveXControl в диалоге?&quot;. Нельзя! Негде для диалога написать &quot;void onEvent_SelectionChange(){}&quot;... (Ax 3.0 SP4)<br />
<br />
Или всё-таки можно? ;) Может, как-то в более новых версиях?..</div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=50</guid>
		</item>
		<item>
			<title>Повышение комфортности разработки при использовании Spreadsheet</title>
			<link>//axforum.info/forums/blog.php?b=48</link>
			<pubDate>Thu, 12 Nov 2009 13:51:03 GMT</pubDate>
			<description><![CDATA[Считая себя увлеченным популяризатором Excel и всего, что на нее (него) похоже, развиваю дальше тему OWC Spreadsheet.

Если решено использовать Spreadsheet для определенной задачи в приложение Axapta (например, отображать в диалоге, как было показано в моем предыдущем собщении: http://www.axforum.info/forums/blog.php?b=26), то управлять его свойствами при программировании приходится практически вслепую. Такое достижение программирующего человечества, как технология IntelliSense, обеспечивающая автоматическое "дописывание" операторов, имен свойств, методов и событий по мере их ввода, а также открывающая список элементов объекта при завершении его имени точкой ".", увы, не работает для переменных типа COM. 

Приходится поступать так: записывать скрипт в Excel (вручную или макрорекордером), переводить его на язык X++ и далее отлаживать, устраняя отдельные недоразумения (всё же Spreadsheet - не точная копия Excel и имеет в ряде случаев свое специфическое поведение).

Это сообщение ставит целью поменять вышеупомянутые шаги местами и сделать перевод с VBA на X++ самым последним и неутомительным рутинным этапом всей процедуры внедрения кода по "обслуживанию" Spreadsheet в приложении Axapta.

Первыми же (более творческими) этапами предлагается сделать записывание в Excel кода VBA макрорекордером, его ручное подправление при необходимости и "переключение" этого кода на выполнение в Spreadsheet (помещенном в рассматриваемом ниже случае на лист Excel). Попутно также устраняются обнаруженные разночтения между Excel и Spreadsheet, НО (!) пользуясь всей мощью IntelliSence уже для самого Spreadsheet, а не для используемого для "черновых" набросков Excel. Что еще не маловажно - визуально уже в самом Spreadsheet же контролируется прописываемое поведение этого объекта (т.е. во время отладки не нужно всякий раз запускать форму, как это было бы при отладке в Аксапте).

Итак, что собственно? Собственно следующее - вручную нарисуем в Excel элемент управления Spreadsheet и напишем несколько строк VBA по назначению этого элемента управления объектной переменной типа Spreadsheet. Как вы правильно поняли, всё происходит в Excel, Axapta здесь только путеводная звезда, к которой мы плывем со своим отлаженным кодом. Отмечу, что целевым приложением может быть и не Axapta, но любое другое, работающее с элементами ActiveX.

ШАГИ:
* откроем Excel (пусть 2003 - я по-прежнему больше люблю то, что старше версии 2007).
* найдём панель инструментов "Control Toolbox" и нажмем на ней кнопку "More Controls" (с пиктограммой с перекрещенными ключом и молотком)
* в открывшемся списке выберем "MIcrosoft Office Spreadsheet 10.0" (или 11.0 или др.) и нарисуем появившимся крестиком рамку внедряемого объекта прямо на текущем листе Excel
* покинем режим конструирования, отжав кнопку "Design Mode" (с угольником и линейкой) и перейдём в редактор VBA (Alt+F11)
* создадим новый модуль и поместим в него следующий код:


Код:
---------
Sub assignSpreadsheetObjectToVariable()

    Dim ss As Spreadsheet
    
    Set ss = ActiveSheet.OLEObjects(1).Object
    
    
    ' и дальше ведем здесь разработку
    ' с использованием переменной ss
   
    ss.Range("A1").Value = "Мы управляем Spreadsheet'ом из Excel"
    ss.Range(ss.Cells(2, 2), ss.Cells(5, 6)).Interior.ColorIndex = 35
    
End Sub
---------

* запустим этот код (F5) и убедимся, что он работает

.
Всё! Далее в этой же процедуре можно сочинять свою обработку Spreadsheet, которую по окончании перевести на язык X++:

Вложение 23 (//axforum.info/forums/attachment.php?attachmentid=23)]]></description>
			<content:encoded><![CDATA[<div>Считая себя увлеченным популяризатором Excel и всего, что на нее (него) похоже, развиваю дальше тему OWC Spreadsheet.<br />
<br />
Если решено использовать Spreadsheet для определенной задачи в приложение Axapta (например, отображать в диалоге, как было показано в моем предыдущем собщении: <a href="http://www.axforum.info/forums/blog.php?b=26" target="_blank">http://www.axforum.info/forums/blog.php?b=26</a>), то управлять его свойствами при программировании приходится практически вслепую. Такое достижение программирующего человечества, как технология IntelliSense, обеспечивающая автоматическое &quot;дописывание&quot; операторов, имен свойств, методов и событий по мере их ввода, а также открывающая список элементов объекта при завершении его имени точкой &quot;.&quot;, увы, не работает для переменных типа COM. <br />
<br />
Приходится поступать так: записывать скрипт в Excel (вручную или макрорекордером), переводить его на язык X++ и далее отлаживать, устраняя отдельные недоразумения (всё же Spreadsheet - не точная копия Excel и имеет в ряде случаев свое специфическое поведение).<br />
<br />
Это сообщение ставит целью поменять вышеупомянутые шаги местами и сделать перевод с VBA на X++ самым последним и неутомительным рутинным этапом всей процедуры внедрения кода по &quot;обслуживанию&quot; Spreadsheet в приложении Axapta.<br />
<br />
Первыми же (более творческими) этапами предлагается сделать записывание в Excel кода VBA макрорекордером, его ручное подправление при необходимости и &quot;переключение&quot; этого кода на выполнение в Spreadsheet (помещенном в рассматриваемом ниже случае на лист Excel). Попутно также устраняются обнаруженные разночтения между Excel и Spreadsheet, НО (!) пользуясь всей мощью IntelliSence уже для самого Spreadsheet, а не для используемого для &quot;черновых&quot; набросков Excel. Что еще не маловажно - визуально уже в самом Spreadsheet же контролируется прописываемое поведение этого объекта (т.е. во время отладки не нужно всякий раз запускать форму, как это было бы при отладке в Аксапте).<br />
<br />
Итак, что собственно? Собственно следующее - вручную нарисуем в Excel элемент управления Spreadsheet и напишем несколько строк VBA по назначению этого элемента управления объектной переменной типа Spreadsheet. Как вы правильно поняли, всё происходит в Excel, Axapta здесь только путеводная звезда, к которой мы плывем со своим отлаженным кодом. Отмечу, что целевым приложением может быть и не Axapta, но любое другое, работающее с элементами ActiveX.<br />
<br />
ШАГИ:<ul><li>откроем Excel (пусть 2003 - я по-прежнему больше люблю то, что старше версии 2007).</li>
<li>найдём панель инструментов &quot;Control Toolbox&quot; и нажмем на ней кнопку &quot;More Controls&quot; (с пиктограммой с перекрещенными ключом и молотком)</li>
<li>в открывшемся списке выберем &quot;MIcrosoft Office Spreadsheet 10.0&quot; (или 11.0 или др.) и нарисуем появившимся крестиком рамку внедряемого объекта прямо на текущем листе Excel</li>
<li>покинем режим конструирования, отжав кнопку &quot;Design Mode&quot; (с угольником и линейкой) и перейдём в редактор VBA (Alt+F11)</li>
<li>создадим новый модуль и поместим в него следующий код:</li>
</ul><div class="xpp"><div class="smallfont xpp_title">Код:</div><pre class="alt2 xpp_code">Sub assignSpreadsheetObjectToVariable()

    Dim ss As Spreadsheet
    
    Set ss = ActiveSheet.OLEObjects(1).Object
    
    
    ' и дальше ведем здесь разработку
    ' с использованием переменной ss
   
    ss.Range(&quot;A1&quot;).Value = &quot;Мы управляем Spreadsheet'ом из Excel&quot;
    ss.Range(ss.Cells(2, 2), ss.Cells(5, 6)).Interior.ColorIndex = 35
    
End Sub</pre></div><ul><li>запустим этот код (F5) и убедимся, что он работает</li>
</ul>.<br />
Всё! Далее в этой же процедуре можно сочинять свою обработку Spreadsheet, которую по окончании перевести на язык X++:<br />
<br />
<a href="//axforum.info/forums/blog_attachment.php?attachmentid=23&amp;d=1258032086" rel="Lightbox" id="attachment23" ><img src="//axforum.info/forums/blog_attachment.php?attachmentid=23&amp;thumb=1&amp;d=1258032086" class="thumbnail" border="0" alt="Нажмите на изображение для увеличения
Название: Spreadsheet_IntelliSence.JPG
Просмотров: 2305
Размер:	29.0 Кб
ID:	23" style="margin: 2px" /></a></div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=48</guid>
		</item>
		<item>
			<title>Использование OWC Spreadsheet в диалоге</title>
			<link>//axforum.info/forums/blog.php?b=26</link>
			<pubDate>Thu, 15 Oct 2009 13:49:39 GMT</pubDate>
			<description><![CDATA[Имеется диалог, при помощи которого пользователь задает параметры изменения срока службы у нескольких основных средств, которые в данный момент отфильтрованы и/или отмечены в гриде формы "Основные средства" (RAssetTable). Процесс инициируется кнопкой, расположенной на этой форме. Диалог выглядит так:

Вложение 8 (//axforum.info/forums/attachment.php?attachmentid=8)

После нажатия на кнопку ОК новое значение срока службы (как можно догадаться из рисунка - одно и то же) прописывается всем выбранным ОС - одному или нескольким, отмеченным в гриде при помощи мышки и клавиш Ctrl или Shift. Код, выполняющий эту работу, выглядит следующим образом ( (с) GMCS ):

void ChangeLifeTime()
{
    DialogRunBase           dialog = new Dialog();
    DialogField             dialogDate,dialogLifeTime,dialogStandardId;

    RAssetLifeTime          dialogLifeTimeValue;
    RAssetStandardId        dialogStandardValue;
    TransDate               dialogDateValue;
    int                     i = 0;
    common                  сurRec;
    RassetTable             сurRAssetTable;
    RassetStandards         сurRassetStandards;
    ;
    сurRec = RAssetTable_ds.getFirst(1);
    if (! сurRec) сurRec = RAssetTable_ds.cursor();
    if (сurRec)
    {
        dialog.caption("Новые значения");
        DialogDate = dialog.addField(typeId(TransDate));
        DialogDate.value(today());
        dialogStandardId = dialog.addField(typeId(RAssetStandardId));
        dialogLifeTime = dialog.addField(typeId(RAssetLifeTime));

        dialogDate.widthMode(formwidth::ColumnWidth);
        dialogStandardId.widthMode(formwidth::ColumnWidth);
        dialogLifeTime.widthMode(formwidth::ColumnWidth);

        if (dialog.run())
        {
            dialogDateValue =           DialogDate.value();
            dialogStandardValue =       dialogStandardId.value();
            dialogLifeTimeValue =       dialogLifeTime.value();
            if (! (dialogDateValue && dialogStandardValue && dialogLifeTimeValue))
            {
                checkFailed(strFmt("Заполните все поля"));
                return;
            }
            if (! RAssetStandardTable::Find(dialogStandardValue))
            {
                checkFailed(strFmt("Значение '%1' не найдено в таблице модели учета", dialogStandardValue));
                return;
            }

            While (сurRec)
            {
                i++;
                RAssetTable_ds.findRecord(сurRec);
                if (RAssetTable_ds)
                {
                    historyDialog.lifetimeDate(dialogDateValue);
                    ttsBegin;
                    сurRassetStandards = RassetStandards::find(
                        сurRec.(fieldNum(RAssetTable,AccountNum)),dialogStandardValue, true);
                    сurRassetStandards.Lifetime = dialogLifeTimeValue;
                    сurRassetStandards.update(historyDialog);
                    ttsCommit;
                    RAssetTable_ds.reread();
                    RAssetTable_ds.refresh();
                }

                сurRec = RAssetTable_ds.getNext();
            }
            info(strFmt("Операция по смене срока службы по %1 ОС  модели %2 успешно завершена", int2Str(i),dialogStandardValue));

        }
    }
}
Помимо изменения поля LifeTime в таблице RAssetStandards для соответствующего ОС, происходит еще и сохранение записи в таблице истории RAssetLifeHist. Переменная historyDialog класса RAssetHistoryDialog определена в ClassDeclaration формы RAssetTable и предназначена для передачи в метод update таблицы RAssetStandards, в котором в конечном счете и выполняется вызов метода, отвечающего за сохранение в истории записи об изменении срока службы ОС. 

Некоторое время назад нашим пользователям потребовалось внести изменения по срокам для нескольких средств, причем для каждого - разными индивидуальными значениями. И процедура обещает повторяться в будущем. 

Нужно было как-то модифицировать исходный диалог, чтобы приспособить его к новым требованиям. Был выбран вариант с ActiveX - OWC Spreradsheet, и после внесения изменений диалог стал выглядеть так:

Вложение 9 (//axforum.info/forums/attachment.php?attachmentid=9)

В связи с тем, что помещение элемента управления ActiveX в диалог не столь прозрачно, как, скажем, помещение текстового поля или поля даты, ниже будет приведен модифицированный код метода ChangeLifeTime, который обслуживает вторую картинку. 

Код будет приведен в первом комментарии, так как попытка поместить его в это стартовое сообщение опять упёрлась в ограничение на 10К символов. В свою очередь, в комментариях не хотят размещаться картинки, но вроде я уже все необходимые разместил в этом сообщении...]]></description>
			<content:encoded><![CDATA[<div>Имеется диалог, при помощи которого пользователь задает параметры изменения срока службы у нескольких основных средств, которые в данный момент отфильтрованы и/или отмечены в гриде формы &quot;Основные средства&quot; (RAssetTable). Процесс инициируется кнопкой, расположенной на этой форме. Диалог выглядит так:<br />
<br />
<img src="//axforum.info/forums/blog_attachment.php?attachmentid=8&amp;d=1253687704" border="0" alt="Название: LifeTimeOld.PNG
Просмотров: 23934

Размер: 2.7 Кб" style="margin: 2px" /><br />
<br />
После нажатия на кнопку ОК новое значение срока службы (как можно догадаться из рисунка - одно и то же) прописывается всем выбранным ОС - одному или нескольким, отмеченным в гриде при помощи мышки и клавиш Ctrl или Shift. Код, выполняющий эту работу, выглядит следующим образом ( (с) GMCS ):<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">void</span> ChangeLifeTime()
{
    DialogRunBase           dialog = <span style="color: blue">new</span> Dialog();
    DialogField             dialogDate,dialogLifeTime,dialogStandardId;

    RAssetLifeTime          dialogLifeTimeValue;
    RAssetStandardId        dialogStandardValue;
    TransDate               dialogDateValue;
    <span style="color: blue">int</span>                     i = 0;
    common                  urRec;
    RassetTable             urRAssetTable;
    RassetStandards         urRassetStandards;
    ;
    urRec = RAssetTable_ds.getFirst(1);
    <span style="color: blue">if</span> (! urRec) urRec = RAssetTable_ds.cursor();
    <span style="color: blue">if</span> (urRec)
    {
        dialog.caption(<span style="color: red">&quot;Новые значения&quot;</span>);
        DialogDate = dialog.addField(<span style="color: blue">typeId</span>(TransDate));
        DialogDate.value(today());
        dialogStandardId = dialog.addField(<span style="color: blue">typeId</span>(RAssetStandardId));
        dialogLifeTime = dialog.addField(<span style="color: blue">typeId</span>(RAssetLifeTime));

        dialogDate.widthMode(formwidth::ColumnWidth);
        dialogStandardId.widthMode(formwidth::ColumnWidth);
        dialogLifeTime.widthMode(formwidth::ColumnWidth);

        <span style="color: blue">if</span> (dialog.run())
        {
            dialogDateValue =           DialogDate.value();
            dialogStandardValue =       dialogStandardId.value();
            dialogLifeTimeValue =       dialogLifeTime.value();
            <span style="color: blue">if</span> (! (dialogDateValue &amp;&amp; dialogStandardValue &amp;&amp; dialogLifeTimeValue))
            {
                checkFailed(strFmt(<span style="color: red">&quot;Заполните все поля&quot;</span>));
                <span style="color: blue">return</span>;
            }
            <span style="color: blue">if</span> (! RAssetStandardTable::Find(dialogStandardValue))
            {
                checkFailed(strFmt(<span style="color: red">&quot;Значение '%1' не найдено в таблице модели учета&quot;</span>, dialogStandardValue));
                <span style="color: blue">return</span>;
            }

            <span style="color: blue">While</span> (urRec)
            {
                i++;
                RAssetTable_ds.findRecord(urRec);
                <span style="color: blue">if</span> (RAssetTable_ds)
                {
                    historyDialog.lifetimeDate(dialogDateValue);
                    <span style="color: blue">ttsBegin</span>;
                    urRassetStandards = RassetStandards::find(
                        urRec.(<span style="color: blue">fieldNum</span>(RAssetTable,AccountNum)),dialogStandardValue, <span style="color: blue">true</span>);
                    urRassetStandards.Lifetime = dialogLifeTimeValue;
                    urRassetStandards.update(historyDialog);
                    <span style="color: blue">ttsCommit</span>;
                    RAssetTable_ds.reread();
                    RAssetTable_ds.refresh();
                }

                urRec = RAssetTable_ds.getNext();
            }
            info(strFmt(<span style="color: red">&quot;Операция по смене срока службы по %1 ОС  модели %2 успешно завершена&quot;</span>, int2Str(i),dialogStandardValue));

        }
    }
}</pre></div>Помимо изменения поля LifeTime в таблице RAssetStandards для соответствующего ОС, происходит еще и сохранение записи в таблице истории RAssetLifeHist. Переменная historyDialog класса RAssetHistoryDialog определена в ClassDeclaration формы RAssetTable и предназначена для передачи в метод update таблицы RAssetStandards, в котором в конечном счете и выполняется вызов метода, отвечающего за сохранение в истории записи об изменении срока службы ОС. <br />
<br />
Некоторое время назад нашим пользователям потребовалось внести изменения по срокам для нескольких средств, причем для каждого - разными индивидуальными значениями. И процедура обещает повторяться в будущем. <br />
<br />
Нужно было как-то модифицировать исходный диалог, чтобы приспособить его к новым требованиям. Был выбран вариант с ActiveX - OWC Spreradsheet, и после внесения изменений диалог стал выглядеть так:<br />
<br />
<a href="//axforum.info/forums/blog_attachment.php?attachmentid=9&amp;d=1253689477" rel="Lightbox" id="attachment9" ><img src="//axforum.info/forums/blog_attachment.php?attachmentid=9&amp;thumb=1&amp;d=1253689477" class="thumbnail" border="0" alt="Нажмите на изображение для увеличения
Название: LifeTimeNew.PNG
Просмотров: 617
Размер:	19.1 Кб
ID:	9" style="margin: 2px" /></a><br />
<br />
В связи с тем, что помещение элемента управления ActiveX в диалог не столь прозрачно, как, скажем, помещение текстового поля или поля даты, ниже будет приведен модифицированный код метода ChangeLifeTime, который обслуживает вторую картинку. <br />
<br />
Код будет приведен в первом комментарии, так как попытка поместить его в это стартовое сообщение опять упёрлась в ограничение на 10К символов. В свою очередь, в комментариях не хотят размещаться картинки, но вроде я уже все необходимые разместил в этом сообщении...</div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=26</guid>
		</item>
		<item>
			<title>Функция определения рабочего статуса дня</title>
			<link>//axforum.info/forums/blog.php?b=13</link>
			<pubDate>Fri, 28 Aug 2009 11:18:27 GMT</pubDate>
			<description><![CDATA[В системах, с которыми мне довелось работать довольно близко, включая Аксапту, БОСС-Кадровик, некоторые бухгалтерские и биллинговые самописки и т.п., как правило, имеется специальная таблица-календарь, из которой черпается информация о рабочих и выходных днях. Одна запись в такой таблице соответствует одному календарному дню. 

Обычно в календаре имеются записи от даты начала работы предприятия или от некоторой другой оговоренной даты, например, с момента старта новой внедренной системы - это то, что касается прошлого. Распространение календаря в будущее не ограничено, но фактически содержит список дней на несколько ближайших лет (5-10, сколько надо исходя из потребностей предприятия). По мере продвижения в будущее в список добавляются дни последующих лет, например, системным администратором, сгенерившем последовательность дней очередного требуемого года в Excel и затем вставившему эти данные в таблицу-календарь, попутно проставив признаки выходных и праздничных дней, а также учтя нестандатные переносы, указанные в очередном ежегодном Постановлении Правительства РФ.

Каждый раз сталкиваясь близко с таким календарем, я невольно задумывался: почему таблица? почему не функция? Ведь существует же, например, в том же Excel функция WORKDAY (РАБДЕНЬ), рассчитывающая дату, отстоящую от исходной на заданное количество рабочих дней. Эта функция даже учтёт праздники, если ей их передать параметром в виде массива дат, правда, не совсем точно для российской реальности - на переносе праздников, попадающих на субботу и воскресенье, на ближайший следующий рабочий день, который предусмотрен статьей 112 нашего Трудового кодекса,  функция WORKDAY сдается ("Ааа, блин!" - сказали суровые сибирские мужики ;) ) Однако, сама попытка создания подобной универсальной функции, вызывает заслуженное уважение в адрес команды разработчиков Excel.

При обучении в институте меня, признаться, несколько утомляли аналитические способы решения интегралов. Поэтому когда я приобщился к вычислительной технике и постиг азы численных методов, я с удовольствием перестал напрягать себя аналитически и вычислял интегралы на машине при помощи метода Симпсона или даже трапеций. Примерно также, мне кажется, не стали напрягать себя и разработчики систем, использующие таблицу-календарь вместо функции. В общем-то, понятно - с таблицей как-то нагляднее и надежнее. К тому же структуру записи можно расширить какими-нибудь дополнительными полями и, таким образом, как бы "превзойти" возможности функции. Опять же количество записей в календаре аж на целый век по современным меркам совсем невелико - чуть меньше 37 тысяч.

Казалось бы, смирись, используй таблицу-календарь и не мучайся! Тем не менее, смутное желание создать функцию, учитывающую российские нюансы переносов праздников, периодически теребило меня несколько лет, но каждый раз начавшийся было процесс раздумий прерывался чем-то более насущным и не оставлял после себя никаких набросков, которые можно было бы развить дальше при следующем приступе "заболевания".

Очередное напоминание явилось в виде темы Как учесть только рабочие часы? (http://www.axforum.info/forums/showthread.php?p=203119#post203119), и я почувствовал, что в этот раз должно что-то оформиться... И оно-таки оформилось! Прекрасно понимая, что родившаяся функция представляет скорее теоретический интерес - кто ж откажется от своих таблиц-календарей, которые за годы обросли связями с самыми различными фрагментами системы?! - я все же надеюсь, что кому-то она может пригодиться и для практического дела. 

Лично мне она понравилась своей компактностью - мне почему-то представлялось, что в подобной функции всяких IF-ов должно быть на порядок больше, чем оно оказалось на самом деле. Может быть, конечно, это из-за того, что в Аксапте есть такие достойнейшие средства, как Map и Set, и без оных как раз куча IF-ов бы и получилась?

Во всяком случае я пока не думал о портации функции в другие языки. Может, через несколько лет... :D

В рамках демонстрационного примера функция (dayWorkingStatus) вложена в нижеследующий джоб, результатом работы которого будет вывод в окно infolog всех дат годов 2007,2008, 2009 с указанием дня недели и рабочего статуса дня:

#macrolib.HolidayExceptions // глобальный макрос (см. ниже)

static void Job_RussianHolidays(Args _args)
{
    date    currDate;
    Map     holidayExceptions = new Map(Types::Date, Types::Integer);

    //------------------------------------------------------------------------
    int dayWorkingStatus(date   _currDate,
                         Map    _exceptions = null,
                         Set    _holidays   = null)
    {
        Set holidaysBefore      = new Set(Types::Date);
        Set holidaysTransfer    = new Set(Types::Date);

        SetEnumerator enumr;
        int holiYear, holiMonth, holiDay;
        date holidayToMove;
        ;

        // самыми первыми проверяем исключения
        // если искомый день среди них, то заканчиваем и возвращаем статус
        if (_exceptions)
        {
            if (_exceptions.exists(_currDate))
                return _exceptions.lookup(_currDate);
        }

        if (!_holidays)
        {
            _holidays = new Set(Types::Container);
            // Russian holidays
            _holidays.add([ 1, 1]); // Новогодние каникулы
            _holidays.add([ 1, 2]); // Новогодние каникулы
            _holidays.add([ 1, 3]); // Новогодние каникулы
            _holidays.add([ 1, 4]); // Новогодние каникулы
            _holidays.add([ 1, 5]); // Новогодние каникулы
            _holidays.add([ 1, 7]); // Рождество Христово
            _holidays.add([ 2,23]); // День защитника Отечества
            _holidays.add([ 3, 8]); // Международный женский день
            _holidays.add([ 5, 1]); // Праздник Весны и Труда
            _holidays.add([ 5, 9]); // День Победы
            _holidays.add([ 6,12]); // День России
            _holidays.add([11, 4]); // День народного единства
        }

        if ( dayOfWk(_currDate)==6 || dayOfWk(_currDate)==7 ||
            _holidays.in([mthOfYr(_currDate), dayOfMth(_currDate)]))
            return 0; // текущий - выходной (сб,вс или празд)

        // если уж дошли сюда, то проверям нет ли праздничного переноса

        // для этого пробегаем по всему множеству "безгодовых" праздников (_holidays)
        // и формируем последовательность всех праздничных дат до текущей даты (holidaysBefore),
        // т.е. генерим реальные праздничные даты за годовой период, предшествующий текущей дате
        enumr = _holidays.getEnumerator();
        while (enumr.moveNext())
        {
            [holiMonth, holiDay] = enumr.current();
            holiYear = ([mthOfYr(_currDate), dayOfMth(_currDate)] > [holiMonth, holiDay]) ?
                            (year(_currDate)) :
                            (year(_currDate)-1);
            holidaysBefore.add(mkDate(holiDay, holiMonth, holiYear));
        }

        // обрабатываем сдвиги выходных дней, вызванных попаданием праздника на субботу или воскрсенье (на 6 или 7)
        enumr = holidaysBefore.getEnumerator();
        while (enumr.moveNext())
        {
            holidayToMove = enumr.current();

            // смещение происходит только в том случае, если выходной попадает на 6 или 7
            while (dayOfWk(holidayToMove)==6 || dayOfWk(holidayToMove)==7)
            {
                if      (dayOfWk(holidayToMove)==6) holidayToMove += 2;
                else if (dayOfWk(holidayToMove)==7) holidayToMove += 1;

                // если перемещаясь попали на другой  праздничный день из НЕСМЕЩЕННЫХ (before)
                // или из УЖЕ СМЕЩЕННЫХ (transfer), то двигаемся дальше
                while ( holidaysBefore  .in(holidayToMove)  ||
                        holidaysTransfer.in(holidayToMove)  )
                    holidayToMove += 1;
            }
            // только если было смещение, добавляем день в множество переносов
            if (! holidaysBefore.in(holidayToMove))
                holidaysTransfer.add(holidayToMove);
        }
        // в результате имеем множество дат с (годами), которые стали выходными из-за предшествующих праздников

        // окончательно определяем рабочийСтатусДня
        if (holidaysTransfer.in(_currDate))
            return 0; // выходной = день, на который перенесли праздник, попавший на 6 или 7
        else
        {
            if (_holidays.in([mthOfYr(_currDate+1), dayOfMth(_currDate+1)]))
                return 2; // рабочий ПРЕДпраздничный
            else
                return 1; // обычный рабочий;
        }
    }
    //------------------------------------------------------------------------
    ;

    #holidayExceptionsInsert(holidayExceptions) // передаем map макросу 

    for (currDate=01\01\2007; currDate<=31\12\2009; currDate++)
    {
        info(strFmt('%1 -- %2 -- %3',
            currDate, dayOfWk(currDate), dayWorkingStatus(currDate, holidayExceptions)));
    }
}
Возвращаемые int-значения ("статусы рабочего дня"): 
* 0 - выходной день,
* 1 - рабочий день,
* 2 - предпраздничный рабочий день.

Параметры: 
* _currDate - дата, для которой определяется статус,
* _exceptions - список переносов-исключений, которые не поддаются алгоритму статьи 112 российского Трудового кодекса; если опущен, то работаем без исключений.
* _holidays - список общегосударственных праздников (только день и месяц); если опущен, то подразумеваются праздники России (прописаны в коде).

Способ формирования списка переносов-исключений - по желанию разработчика: или читать из специальной таблицы; или использовать макрос, который по мере продвижения вперед по годам следует дополнять новыми строками - на основании информации из очередного Постановления. 

(to be continued...)]]></description>
			<content:encoded><![CDATA[<div>В системах, с которыми мне довелось работать довольно близко, включая Аксапту, БОСС-Кадровик, некоторые бухгалтерские и биллинговые самописки и т.п., как правило, имеется специальная таблица-календарь, из которой черпается информация о рабочих и выходных днях. Одна запись в такой таблице соответствует одному календарному дню. <br />
<br />
Обычно в календаре имеются записи от даты начала работы предприятия или от некоторой другой оговоренной даты, например, с момента старта новой внедренной системы - это то, что касается прошлого. Распространение календаря в будущее не ограничено, но фактически содержит список дней на несколько ближайших лет (5-10, сколько надо исходя из потребностей предприятия). По мере продвижения в будущее в список добавляются дни последующих лет, например, системным администратором, сгенерившем последовательность дней очередного требуемого года в Excel и затем вставившему эти данные в таблицу-календарь, попутно проставив признаки выходных и праздничных дней, а также учтя нестандатные переносы, указанные в очередном ежегодном Постановлении Правительства РФ.<br />
<br />
Каждый раз сталкиваясь близко с таким календарем, я невольно задумывался: почему таблица? почему не функция? Ведь существует же, например, в том же Excel функция WORKDAY (РАБДЕНЬ), рассчитывающая дату, отстоящую от исходной на заданное количество рабочих дней. Эта функция даже учтёт праздники, если ей их передать параметром в виде массива дат, правда, не совсем точно для российской реальности - на переносе праздников, попадающих на субботу и воскресенье, на ближайший следующий рабочий день, который предусмотрен статьей 112 нашего Трудового кодекса,  функция WORKDAY сдается (&quot;Ааа, блин!&quot; - сказали суровые сибирские мужики ;) ) Однако, сама попытка создания подобной универсальной функции, вызывает заслуженное уважение в адрес команды разработчиков Excel.<br />
<br />
При обучении в институте меня, признаться, несколько утомляли аналитические способы решения интегралов. Поэтому когда я приобщился к вычислительной технике и постиг азы численных методов, я с удовольствием перестал напрягать себя аналитически и вычислял интегралы на машине при помощи метода Симпсона или даже трапеций. Примерно также, мне кажется, не стали напрягать себя и разработчики систем, использующие таблицу-календарь вместо функции. В общем-то, понятно - с таблицей как-то нагляднее и надежнее. К тому же структуру записи можно расширить какими-нибудь дополнительными полями и, таким образом, как бы &quot;превзойти&quot; возможности функции. Опять же количество записей в календаре аж на целый век по современным меркам совсем невелико - чуть меньше 37 тысяч.<br />
<br />
Казалось бы, смирись, используй таблицу-календарь и не мучайся! Тем не менее, смутное желание создать функцию, учитывающую российские нюансы переносов праздников, периодически теребило меня несколько лет, но каждый раз начавшийся было процесс раздумий прерывался чем-то более насущным и не оставлял после себя никаких набросков, которые можно было бы развить дальше при следующем приступе &quot;заболевания&quot;.<br />
<br />
Очередное напоминание явилось в виде темы <a href="http://www.axforum.info/forums/showthread.php?p=203119#post203119" target="_blank">Как учесть только рабочие часы?</a>, и я почувствовал, что в этот раз должно что-то оформиться... И оно-таки оформилось! Прекрасно понимая, что родившаяся функция представляет скорее теоретический интерес - кто ж откажется от своих таблиц-календарей, которые за годы обросли связями с самыми различными фрагментами системы?! - я все же надеюсь, что кому-то она может пригодиться и для практического дела. <br />
<br />
Лично мне она понравилась своей компактностью - мне почему-то представлялось, что в подобной функции всяких IF-ов должно быть на порядок больше, чем оно оказалось на самом деле. Может быть, конечно, это из-за того, что в Аксапте есть такие достойнейшие средства, как Map и Set, и без оных как раз куча IF-ов бы и получилась?<br />
<br />
Во всяком случае я пока не думал о портации функции в другие языки. Может, через несколько лет... :D<br />
<br />
В рамках демонстрационного примера функция (dayWorkingStatus) вложена в нижеследующий джоб, результатом работы которого будет вывод в окно infolog всех дат годов 2007,2008, 2009 с указанием дня недели и рабочего статуса дня:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">#macrolib.HolidayExceptions <span style="color: green">// глобальный макрос (см. ниже)
</span>
<span style="color: blue">static</span> <span style="color: blue">void</span> Job_RussianHolidays(Args _args)
{
    <span style="color: blue">date</span>    currDate;
    Map     holidayExceptions = <span style="color: blue">new</span> Map(Types::<span style="color: blue">Date</span>, Types::Integer);

    <span style="color: green">//------------------------------------------------------------------------
</span>    <span style="color: blue">int</span> dayWorkingStatus(<span style="color: blue">date</span>   _currDate,
                         Map    _exceptions = <span style="color: blue">null</span>,
                         Set    _holidays   = <span style="color: blue">null</span>)
    {
        Set holidaysBefore      = <span style="color: blue">new</span> Set(Types::<span style="color: blue">Date</span>);
        Set holidaysTransfer    = <span style="color: blue">new</span> Set(Types::<span style="color: blue">Date</span>);

        SetEnumerator enumr;
        <span style="color: blue">int</span> holiYear, holiMonth, holiDay;
        <span style="color: blue">date</span> holidayToMove;
        ;

        <span style="color: green">// самыми первыми проверяем исключения
</span>        <span style="color: green">// если искомый день среди них, то заканчиваем и возвращаем статус
</span>        <span style="color: blue">if</span> (_exceptions)
        {
            <span style="color: blue">if</span> (_exceptions.<span style="color: blue">exists</span>(_currDate))
                <span style="color: blue">return</span> _exceptions.lookup(_currDate);
        }

        <span style="color: blue">if</span> (!_holidays)
        {
            _holidays = <span style="color: blue">new</span> Set(Types::<span style="color: blue">Container</span>);
            <span style="color: green">// Russian holidays
</span>            _holidays.add([ 1, 1]); <span style="color: green">// Новогодние каникулы
</span>            _holidays.add([ 1, 2]); <span style="color: green">// Новогодние каникулы
</span>            _holidays.add([ 1, 3]); <span style="color: green">// Новогодние каникулы
</span>            _holidays.add([ 1, 4]); <span style="color: green">// Новогодние каникулы
</span>            _holidays.add([ 1, 5]); <span style="color: green">// Новогодние каникулы
</span>            _holidays.add([ 1, 7]); <span style="color: green">// Рождество Христово
</span>            _holidays.add([ 2,23]); <span style="color: green">// День защитника Отечества
</span>            _holidays.add([ 3, 8]); <span style="color: green">// Международный женский день
</span>            _holidays.add([ 5, 1]); <span style="color: green">// Праздник Весны и Труда
</span>            _holidays.add([ 5, 9]); <span style="color: green">// День Победы
</span>            _holidays.add([ 6,12]); <span style="color: green">// День России
</span>            _holidays.add([11, 4]); <span style="color: green">// День народного единства
</span>        }

        <span style="color: blue">if</span> ( dayOfWk(_currDate)==6 || dayOfWk(_currDate)==7 ||
            _holidays.in([mthOfYr(_currDate), dayOfMth(_currDate)]))
            <span style="color: blue">return</span> 0; <span style="color: green">// текущий - выходной (сб,вс или празд)
</span>
        <span style="color: green">// если уж дошли сюда, то проверям нет ли праздничного переноса
</span>
        <span style="color: green">// для этого пробегаем по всему множеству &quot;безгодовых&quot; праздников (_holidays)
</span>        <span style="color: green">// и формируем последовательность всех праздничных дат до текущей даты (holidaysBefore),
</span>        <span style="color: green">// т.е. генерим реальные праздничные даты за годовой период, предшествующий текущей дате
</span>        enumr = _holidays.getEnumerator();
        <span style="color: blue">while</span> (enumr.moveNext())
        {
            [holiMonth, holiDay] = enumr.current();
            holiYear = ([mthOfYr(_currDate), dayOfMth(_currDate)] &gt; [holiMonth, holiDay]) ?
                            (year(_currDate)) :
                            (year(_currDate)-1);
            holidaysBefore.add(mkDate(holiDay, holiMonth, holiYear));
        }

        <span style="color: green">// обрабатываем сдвиги выходных дней, вызванных попаданием праздника на субботу или воскрсенье (на 6 или 7)
</span>        enumr = holidaysBefore.getEnumerator();
        <span style="color: blue">while</span> (enumr.moveNext())
        {
            holidayToMove = enumr.current();

            <span style="color: green">// смещение происходит только в том случае, если выходной попадает на 6 или 7
</span>            <span style="color: blue">while</span> (dayOfWk(holidayToMove)==6 || dayOfWk(holidayToMove)==7)
            {
                <span style="color: blue">if</span>      (dayOfWk(holidayToMove)==6) holidayToMove += 2;
                <span style="color: blue">else</span> <span style="color: blue">if</span> (dayOfWk(holidayToMove)==7) holidayToMove += 1;

                <span style="color: green">// если перемещаясь попали на другой  праздничный день из НЕСМЕЩЕННЫХ (before)
</span>                <span style="color: green">// или из УЖЕ СМЕЩЕННЫХ (transfer), то двигаемся дальше
</span>                <span style="color: blue">while</span> ( holidaysBefore  .in(holidayToMove)  ||
                        holidaysTransfer.in(holidayToMove)  )
                    holidayToMove += 1;
            }
            <span style="color: green">// только если было смещение, добавляем день в множество переносов
</span>            <span style="color: blue">if</span> (! holidaysBefore.in(holidayToMove))
                holidaysTransfer.add(holidayToMove);
        }
        <span style="color: green">// в результате имеем множество дат с (годами), которые стали выходными из-за предшествующих праздников
</span>
        <span style="color: green">// окончательно определяем рабочийСтатусДня
</span>        <span style="color: blue">if</span> (holidaysTransfer.in(_currDate))
            <span style="color: blue">return</span> 0; <span style="color: green">// выходной = день, на который перенесли праздник, попавший на 6 или 7
</span>        <span style="color: blue">else</span>
        {
            <span style="color: blue">if</span> (_holidays.in([mthOfYr(_currDate+1), dayOfMth(_currDate+1)]))
                <span style="color: blue">return</span> 2; <span style="color: green">// рабочий ПРЕДпраздничный
</span>            <span style="color: blue">else</span>
                <span style="color: blue">return</span> 1; <span style="color: green">// обычный рабочий;
</span>        }
    }
    <span style="color: green">//------------------------------------------------------------------------
</span>    ;

    #holidayExceptionsInsert(holidayExceptions) <span style="color: green">// передаем map макросу 
</span>
    <span style="color: blue">for</span> (currDate=01\01\2007; currDate&lt;=31\12\2009; currDate++)
    {
        info(strFmt(<span style="color: red">'%1 -- %2 -- %3'</span>,
            currDate, dayOfWk(currDate), dayWorkingStatus(currDate, holidayExceptions)));
    }
}</pre></div>Возвращаемые int-значения (&quot;статусы рабочего дня&quot;): <ul><li>0 - выходной день,</li>
<li>1 - рабочий день,</li>
<li>2 - предпраздничный рабочий день.</li>
</ul>Параметры: <ul><li>_currDate - дата, для которой определяется статус,</li>
<li>_exceptions - список переносов-исключений, которые не поддаются алгоритму статьи 112 российского Трудового кодекса; если опущен, то работаем без исключений.</li>
<li>_holidays - список общегосударственных праздников (только день и месяц); если опущен, то подразумеваются праздники России (прописаны в коде).</li>
</ul>Способ формирования списка переносов-исключений - по желанию разработчика: или читать из специальной таблицы; или использовать макрос, который по мере продвижения вперед по годам следует дополнять новыми строками - на основании информации из очередного Постановления. <br />
<br />
(to be continued...)</div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=13</guid>
		</item>
		<item>
			<title>Я осваиваю Office 2007</title>
			<link>//axforum.info/forums/blog.php?b=14</link>
			<pubDate>Thu, 20 Aug 2009 11:15:29 GMT</pubDate>
			<description><![CDATA[Наконец, свершилось. На одном из доступных мне компьютеров появился Office 2007. А то уже было стыдно просить друзей по переписке перепослать какой-либо файл Excel в прежнем формате 97-2003, так как я не мог открыть файл формата 2007.

Тем не менее, в качестве нашего корпоративного стандарта ПО Office 2007 еще не утвержден и многие пользователи всё еще продолжают сидеть на Excel 2003 и даже 2000. Поэтому надеюсь в будущем помочь им своими заметками, которые я буду собирать в этом сообщении.

Попутно мне подумалось, что, возможно, именно данная тема может быть идеальной для подобного рода блога: во-первых, напрямую с Аксаптой не связана (а то мягкие упрёки "что не в Форуме" уже появились: http://www.axforum.info/forums/blog.php?bt=47); во-вторых, многим, уже перешедшим на Office 2007, скорее всего, уже не интересная, но зато интересная мне и далее моим пользователям как напоминалка об основных операциях в "новом формате". 

Речь в первую очередь пойдёт о существенно изменившемся интерфейсе приложений. А среди приложений я буду в основном "налегать" на Excel. Приглашаю всех желающих поделиться своими подробностями перехода на версию 2007 участвовать в "Комментариях" к данному посту. 

Итак, поехали! Для Excel:

* *Есть ли возможность включить меню в традиционном стиле предыдущих версий?* - Штатным образом - НЕТ! И не ищите! Я тоже очень надеялся, но Microsoft решил действовать радикально - "резать к чёртовой матери, не дожидаясь перитонитов" :( Однако существуют сторонние (небесплатные - 15-30 долларов) примочки, позволяющие это сделать (можно поискать в Гугле по строке "старое меню Excel").

* *Как общаться с пользователями предыдущих версий?* - Жмём круглую кнопку "Office" в левом верхнем углу, далее в нижней части открывшегося окна кнопку "Параметры Excel" и далее: Сохранение \ Сохранять файлы в следующем формате \ Книга Excel 97-2003 (*.xls). После рестарта Excel видим, что строк на листах "опять" стало 65 тысяч вместо "новых" более 1 миллиона. В заголовке окна при этом горит подстрока "Режим совместимости".

* *Где спрятаны команды Создать, Открыть, Сохранить?* - Моё знакомство с Excel 2007 уже состоялось несколько раньше, когда меня подозвал начальник и попросил помочь сохранить файл. Тогда я не смог ему помочь! Кто бы мог подумать, что круглая "дура" в левом верхнем углу окна не просто украшение, а кнопка "Кнопка "Office"?? За ней, собственно, и скрываются перечисленные команды, а также некоторые другие :)

* *Если вы записываете и редактируете макросы на VBA, то включите вкладку "Разработчик"* - Кнопка "Office" \ Параметры Excel \ Основные \ Показывать вкладку "Разработчик" на ленте.

* *Одновременное отображение нескольких агрегатных функций в строке состояния* - в предыдущих версиях Excel для выделенного диапазона можно было отобразить что-то одно: либо сумму (пользовалась наибольшей популярностью), либо среднее, либо минимум, либо максимум и т.п. Иногда этого не хватало - например, хотелось видеть одновременно и среднее, и максимум. Теперь это возможно - кликаем на строке состояния правой кнопкой и выставляем любую комбинацию галок: Среднее, Количество, Количество чисел, Минимум, Максимум, Сумма. Причем, галки даже можно не ставить - соответствующие величины уже отображаются прямо в этом контекстном меню.

* *Где теперь галка Tools \ Options \ View \ Windows in Taskbar ?* - Кнопка "Office" \ Параметры Excel \ Дополнительно \ группа Экран \ Показывать все окна на панели задач

* *Как разрешить выполнение макросов?* - выполнение макросов по умолчанию (сразу после установки) запрещено. Разрешить выполнение макросов можно при помощи уже упоминавшейся вкладки "Разработчик": эта вкладка \ группа пиктограмм "Код" (название группы подписано снизу) \ пиктограмма "Безопасность макросов" \ Параметры макросов \ радиокнопка "Включить все макросы"

* *Как найти нужную команду на ленте?* - пока вы еще не привыкли к местоположению команд на ленте и не сразу находите необходимую, можно воспользоваться режимом добавления команд в панель быстрого доступа (саму команду не обязательно при этом добавлять на панель). Щелкните правой кнопкой на ленте и выберите "Настройка панели быстрого доступа". В открывшемся окне "Параметры Excel" в поле со списком "Выбрать команды из..." выберите "Все команды". В отсортированном по алфавиту списке команд найдите нужную и наведите на нее мышь - всплывающая подсказка покажет путь к команде, например, "Вкладка "Разработчик" | Код | Безопасность макросов". Теперь вы знаете, как найти команду "Безопасность". Обратите внимание, что в списке имеются команды, которых нет на ленте. Они - явные кандидаты на помещение в панель быстрого доступа.

* *Теперь можно сортировать по цвету ячейки* - при работе с предыдущими версиями Excel наверняка многим приходилось обрабатывать пользовательские файлы, в которых некоторые отметки были сделаны цветом и далее надо было отобрать эти помеченные записи. Чтобы выполнить сортировку таких ячеек нужно было выделить рядом с ними дополнительный столбец для текстовых отметок и либо просмотреть таблицу вручную (если не очень большая) и пометить строки "единичкой" в этом дополнительном столбце, либо написать несложную функцию рабочего листа, возвращающую код цвета заливки соседней ячейки. Теперь всё проще: вкладка "Главная" \ группа "Редактирование" \ Сортировка и фильтр \ Настраиваемая сортировка.]]></description>
			<content:encoded><![CDATA[<div>Наконец, свершилось. На одном из доступных мне компьютеров появился Office 2007. А то уже было стыдно просить друзей по переписке перепослать какой-либо файл Excel в прежнем формате 97-2003, так как я не мог открыть файл формата 2007.<br />
<br />
Тем не менее, в качестве нашего корпоративного стандарта ПО Office 2007 еще не утвержден и многие пользователи всё еще продолжают сидеть на Excel 2003 и даже 2000. Поэтому надеюсь в будущем помочь им своими заметками, которые я буду собирать в этом сообщении.<br />
<br />
Попутно мне подумалось, что, возможно, именно данная тема может быть идеальной для подобного рода блога: во-первых, напрямую с Аксаптой не связана (а то мягкие упрёки &quot;что не в Форуме&quot; уже появились: <a href="http://www.axforum.info/forums/blog.php?bt=47" target="_blank">http://www.axforum.info/forums/blog.php?bt=47</a>); во-вторых, многим, уже перешедшим на Office 2007, скорее всего, уже не интересная, но зато интересная мне и далее моим пользователям как напоминалка об основных операциях в &quot;новом формате&quot;. <br />
<br />
Речь в первую очередь пойдёт о существенно изменившемся интерфейсе приложений. А среди приложений я буду в основном &quot;налегать&quot; на Excel. Приглашаю всех желающих поделиться своими подробностями перехода на версию 2007 участвовать в &quot;Комментариях&quot; к данному посту. <br />
<br />
Итак, поехали! Для Excel:<br />
<br />
* <b>Есть ли возможность включить меню в традиционном стиле предыдущих версий?</b> - Штатным образом - НЕТ! И не ищите! Я тоже очень надеялся, но Microsoft решил действовать радикально - &quot;резать к чёртовой матери, не дожидаясь перитонитов&quot; :( Однако существуют сторонние (небесплатные - 15-30 долларов) примочки, позволяющие это сделать (можно поискать в Гугле по строке &quot;старое меню Excel&quot;).<br />
<br />
* <b>Как общаться с пользователями предыдущих версий?</b> - Жмём круглую кнопку &quot;Office&quot; в левом верхнем углу, далее в нижней части открывшегося окна кнопку &quot;Параметры Excel&quot; и далее: Сохранение \ Сохранять файлы в следующем формате \ Книга Excel 97-2003 (*.xls). После рестарта Excel видим, что строк на листах &quot;опять&quot; стало 65 тысяч вместо &quot;новых&quot; более 1 миллиона. В заголовке окна при этом горит подстрока &quot;Режим совместимости&quot;.<br />
<br />
* <b>Где спрятаны команды Создать, Открыть, Сохранить?</b> - Моё знакомство с Excel 2007 уже состоялось несколько раньше, когда меня подозвал начальник и попросил помочь сохранить файл. Тогда я не смог ему помочь! Кто бы мог подумать, что круглая &quot;дура&quot; в левом верхнем углу окна не просто украшение, а кнопка &quot;Кнопка &quot;Office&quot;?? За ней, собственно, и скрываются перечисленные команды, а также некоторые другие :)<br />
<br />
* <b>Если вы записываете и редактируете макросы на VBA, то включите вкладку &quot;Разработчик&quot;</b> - Кнопка &quot;Office&quot; \ Параметры Excel \ Основные \ Показывать вкладку &quot;Разработчик&quot; на ленте.<br />
<br />
* <b>Одновременное отображение нескольких агрегатных функций в строке состояния</b> - в предыдущих версиях Excel для выделенного диапазона можно было отобразить что-то одно: либо сумму (пользовалась наибольшей популярностью), либо среднее, либо минимум, либо максимум и т.п. Иногда этого не хватало - например, хотелось видеть одновременно и среднее, и максимум. Теперь это возможно - кликаем на строке состояния правой кнопкой и выставляем любую комбинацию галок: Среднее, Количество, Количество чисел, Минимум, Максимум, Сумма. Причем, галки даже можно не ставить - соответствующие величины уже отображаются прямо в этом контекстном меню.<br />
<br />
* <b>Где теперь галка Tools \ Options \ View \ Windows in Taskbar ?</b> - Кнопка &quot;Office&quot; \ Параметры Excel \ Дополнительно \ группа Экран \ Показывать все окна на панели задач<br />
<br />
* <b>Как разрешить выполнение макросов?</b> - выполнение макросов по умолчанию (сразу после установки) запрещено. Разрешить выполнение макросов можно при помощи уже упоминавшейся вкладки &quot;Разработчик&quot;: эта вкладка \ группа пиктограмм &quot;Код&quot; (название группы подписано снизу) \ пиктограмма &quot;Безопасность макросов&quot; \ Параметры макросов \ радиокнопка &quot;Включить все макросы&quot;<br />
<br />
* <b>Как найти нужную команду на ленте?</b> - пока вы еще не привыкли к местоположению команд на ленте и не сразу находите необходимую, можно воспользоваться режимом добавления команд в панель быстрого доступа (саму команду не обязательно при этом добавлять на панель). Щелкните правой кнопкой на ленте и выберите &quot;Настройка панели быстрого доступа&quot;. В открывшемся окне &quot;Параметры Excel&quot; в поле со списком &quot;Выбрать команды из...&quot; выберите &quot;Все команды&quot;. В отсортированном по алфавиту списке команд найдите нужную и наведите на нее мышь - всплывающая подсказка покажет путь к команде, например, &quot;Вкладка &quot;Разработчик&quot; | Код | Безопасность макросов&quot;. Теперь вы знаете, как найти команду &quot;Безопасность&quot;. Обратите внимание, что в списке имеются команды, которых нет на ленте. Они - явные кандидаты на помещение в панель быстрого доступа.<br />
<br />
* <b>Теперь можно сортировать по цвету ячейки</b> - при работе с предыдущими версиями Excel наверняка многим приходилось обрабатывать пользовательские файлы, в которых некоторые отметки были сделаны цветом и далее надо было отобрать эти помеченные записи. Чтобы выполнить сортировку таких ячеек нужно было выделить рядом с ними дополнительный столбец для текстовых отметок и либо просмотреть таблицу вручную (если не очень большая) и пометить строки &quot;единичкой&quot; в этом дополнительном столбце, либо написать несложную функцию рабочего листа, возвращающую код цвета заливки соседней ячейки. Теперь всё проще: вкладка &quot;Главная&quot; \ группа &quot;Редактирование&quot; \ Сортировка и фильтр \ Настраиваемая сортировка.</div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=14</guid>
		</item>
		<item>
			<title>Контроль возможности создания журнала</title>
			<link>//axforum.info/forums/blog.php?b=10</link>
			<pubDate>Tue, 28 Jul 2009 12:41:19 GMT</pubDate>
			<description><![CDATA[Ну, что... попробуем создать первую, так сказать, содержательную запись. Итак...

Возникла необходимость не разрешать пользователям создание журналов (в различных модулях), если не выполнено некоторое условие. В нашем приложении Axapta от GMCS этим условием является заполненность поля "Первичная группа пользователя" формы "Параметры" (верхнее меню \ Сервис \ Параметры \ вкладка Разное; поле UserGroupDim в таблице SysUserInfo). Значение этого поля фактически определяет конкретный филиал нашего холдинга, к которому принадлежит пользователь. 

Функционал приложения глобально кастомизирован так, что при заполненном поле пользователи видят только данные своего филиала и не видят других (своеобразный аналог механизма RLS). Однако, иногда им необходимо видеть данные всех филиалов одновременно, и они самостоятельно очищают это поле (те, у которых на это есть соответствующее право). 

Неприятности начинаются, когда пользователю снова необходимо переключиться с аналитической работы на операционную и ввести какой-нибудь журнал. Если пользователь забыл включить конкретный филиал, то созданный журнал сохранится и разнесется "вне филиала" со всеми вытекающими последствиями - конечно, не супер-пагубными, но всё же вызывающими раздражение. Коррекцией ситуации обычно занимается системный администратор, который по звонку пользователя, допустившего оплошность, средствами СУБД или джобиком Аксапты прописывает недостающие филиальные данные в журнальные таблицы.

И вот было решено избавить администратора от этих неконструктивных действий и контролировать пользователя при создании журнала. Возник вопрос - в каком конкретном месте этот контроль выполнять? Я не смог найти (или плохо искал) единого предка для всех журналов (какой-нибудь JournalTable-папа), чтобы в каком-либо подобающем методе (new, construct, create, insert и т.п.) которого вставить необходимую проверку. Попробовал самое начало метода construct класса JournalTableData - получилось не очень удачно, ну в смысле - совсем не получилось:

private static JournalTableData construct(
    JournalTableMap _journalTable
    )
{
    // мои эксперименты -->
    ;
    box::info('Привет из констракта');
    // return;
    // мои эксперименты <--

    switch (_journalTable.tableId)
    {
        case tableNum(TutorialJournalTable):    return new tutorialJournalTableData(_journalTable);
        case tableNum(ProjJournalTable):        return new ProjJournalTableData(_journalTable);
        case tableNum(LedgerJournalTable):      return new LedgerJournalTableData(_journalTable);
        case tableNum(WMSJournalTable):         return new WMSJournalTableData(_journalTable);
        case tableNum(InventJournalTable):      return new InventJournalTableData(_journalTable);
        case tableNum(ProdJournalTable):        return prodJournalTableData::newTable(_journalTable);
        case tableNum(RPayJournalTable):        return new RPayJournalTableData(_journalTable);
        case tableNum(RHRMOrderTable):          return new RHRMJournalTableData(_journalTable);

        default :
            return new JournalTableData(_journalTable);
    }
}
Тогда я решил просто навставлять проверочный код в начало метода insert тех таблиц, которые присутствуют в вышеприведенном методе construct, благо что из этого перечня живыми в нашем приложении оказалось всего две: LedgerJournalTable и InventJournalTable.

Сказано - сделано. В начало методов insert двух указанных таблиц был добавлен одинаковый фрагмент:

    if (KKu::userUnknownFilialToCreateJournal(this))
        return; // да - прерываемся
а сам статический метод определен как:

static boolean userUnknownFilialToCreateJournal(Common _common)
{
    #define.alarmJournalDescr('ПОЖАЛУЙСТА, УДАЛИТЕ ЭТУ НЕПРАВИЛЬНУЮ ЗАПИСЬ БЕЗ ФИЛИАЛА!')
    ;
    if (SysUserInfo::find(curUserId()).UserGroupDim)
    {
        return false;
    }
    else
    {
        box::stop(strFmt('%1\n\n%2\n%3\n\n%4\n%5',
            'Невозможно создать журнал - не указан филиал текущего пользователя!',
            'Пожалуйста, укажите филиал в поле "Первичная группа пользователя" формы "Параметры",',
            'после чего начните создание журнала заново.',
            'Если Вы работаете в форме-списке журналов, то удалите неправильную запись без филиала',
            'или закройте и снова откройте форму.'));

        switch (_common.TableId)
        {
            case tableNum(LedgerJournalTable):
                _common.(fieldNum(LedgerJournalTable, Name)) = #alarmJournalDescr;
                break;

            case tableNum(InventJournalTable):
                _common.(fieldNum(InventJournalTable, Description)) = #alarmJournalDescr;
                break;
        }
        return true; // Неизвестный филиал пользователя для создания журнала
    }
}
Получилось, конечно, несколько громоздковато, но, с другой стороны, производимый эффект совпадает с ожиданиями заказчика :)

Происходит следующее. В зависимости от конкретной формы, для которой срабатывает insert, пользователю "без филиала" разрешается создать "запись" в гриде формы, и внешне она выглядит как обычная запись, до тех пор пока пользователь не перейдет на другую запись или не попробует перейти в строки журнала при помощи соответствующей кнопки. Тут и вылетает предупреждение, а поле описания журнала заполняется просьбой об удалении записи. Если пользователь закроет форму и потом откроет ее, то "записи" этой уже не будет.

Буду рад, если кто-нибудь из читателей укажет путь к какому-нибудь более правильному методу решения этой задачи. Я почти уверен, что он существует! :)]]></description>
			<content:encoded><![CDATA[<div>Ну, что... попробуем создать первую, так сказать, содержательную запись. Итак...<br />
<br />
Возникла необходимость не разрешать пользователям создание журналов (в различных модулях), если не выполнено некоторое условие. В нашем приложении Axapta от GMCS этим условием является заполненность поля &quot;Первичная группа пользователя&quot; формы &quot;Параметры&quot; (верхнее меню \ Сервис \ Параметры \ вкладка Разное; поле UserGroupDim в таблице SysUserInfo). Значение этого поля фактически определяет конкретный филиал нашего холдинга, к которому принадлежит пользователь. <br />
<br />
Функционал приложения глобально кастомизирован так, что при заполненном поле пользователи видят только данные своего филиала и не видят других (своеобразный аналог механизма RLS). Однако, иногда им необходимо видеть данные всех филиалов одновременно, и они самостоятельно очищают это поле (те, у которых на это есть соответствующее право). <br />
<br />
Неприятности начинаются, когда пользователю снова необходимо переключиться с аналитической работы на операционную и ввести какой-нибудь журнал. Если пользователь забыл включить конкретный филиал, то созданный журнал сохранится и разнесется &quot;вне филиала&quot; со всеми вытекающими последствиями - конечно, не супер-пагубными, но всё же вызывающими раздражение. Коррекцией ситуации обычно занимается системный администратор, который по звонку пользователя, допустившего оплошность, средствами СУБД или джобиком Аксапты прописывает недостающие филиальные данные в журнальные таблицы.<br />
<br />
И вот было решено избавить администратора от этих неконструктивных действий и контролировать пользователя при создании журнала. Возник вопрос - в каком конкретном месте этот контроль выполнять? Я не смог найти (или плохо искал) единого предка для всех журналов (какой-нибудь JournalTable-папа), чтобы в каком-либо подобающем методе (new, construct, create, insert и т.п.) которого вставить необходимую проверку. Попробовал самое начало метода construct класса JournalTableData - получилось не очень удачно, ну в смысле - совсем не получилось:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">private</span> <span style="color: blue">static</span> JournalTableData construct(
    JournalTableMap _journalTable
    )
{
    <span style="color: green">// мои эксперименты --&gt;
</span>    ;
    box::info(<span style="color: red">'Привет из констракта'</span>);
    <span style="color: green">// return;
</span>    <span style="color: green">// мои эксперименты &lt;--
</span>
    <span style="color: blue">switch</span> (_journalTable.tableId)
    {
        <span style="color: blue">case</span> <span style="color: blue">tableNum</span>(TutorialJournalTable):    <span style="color: blue">return</span> <span style="color: blue">new</span> tutorialJournalTableData(_journalTable);
        <span style="color: blue">case</span> <span style="color: blue">tableNum</span>(ProjJournalTable):        <span style="color: blue">return</span> <span style="color: blue">new</span> ProjJournalTableData(_journalTable);
        <span style="color: blue">case</span> <span style="color: blue">tableNum</span>(LedgerJournalTable):      <span style="color: blue">return</span> <span style="color: blue">new</span> LedgerJournalTableData(_journalTable);
        <span style="color: blue">case</span> <span style="color: blue">tableNum</span>(WMSJournalTable):         <span style="color: blue">return</span> <span style="color: blue">new</span> WMSJournalTableData(_journalTable);
        <span style="color: blue">case</span> <span style="color: blue">tableNum</span>(InventJournalTable):      <span style="color: blue">return</span> <span style="color: blue">new</span> InventJournalTableData(_journalTable);
        <span style="color: blue">case</span> <span style="color: blue">tableNum</span>(ProdJournalTable):        <span style="color: blue">return</span> prodJournalTableData::newTable(_journalTable);
        <span style="color: blue">case</span> <span style="color: blue">tableNum</span>(RPayJournalTable):        <span style="color: blue">return</span> <span style="color: blue">new</span> RPayJournalTableData(_journalTable);
        <span style="color: blue">case</span> <span style="color: blue">tableNum</span>(RHRMOrderTable):          <span style="color: blue">return</span> <span style="color: blue">new</span> RHRMJournalTableData(_journalTable);

        <span style="color: blue">default</span> :
            <span style="color: blue">return</span> <span style="color: blue">new</span> JournalTableData(_journalTable);
    }
}</pre></div>Тогда я решил просто навставлять проверочный код в начало метода insert тех таблиц, которые присутствуют в вышеприведенном методе construct, благо что из этого перечня живыми в нашем приложении оказалось всего две: LedgerJournalTable и InventJournalTable.<br />
<br />
Сказано - сделано. В начало методов insert двух указанных таблиц был добавлен одинаковый фрагмент:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code">    <span style="color: blue">if</span> (KKu::userUnknownFilialToCreateJournal(this))
        <span style="color: blue">return</span>; <span style="color: green">// да - прерываемся</span></pre></div>а сам статический метод определен как:<br />
<div class="xpp"><div class="smallfont xpp_title">X++:</div><pre class="alt2 xpp_code"><span style="color: blue">static</span> boolean userUnknownFilialToCreateJournal(Common _common)
{
    #define.alarmJournalDescr(<span style="color: red">'ПОЖАЛУЙСТА, УДАЛИТЕ ЭТУ НЕПРАВИЛЬНУЮ ЗАПИСЬ БЕЗ ФИЛИАЛА!'</span>)
    ;
    <span style="color: blue">if</span> (SysUserInfo::find(curUserId()).UserGroupDim)
    {
        <span style="color: blue">return</span> <span style="color: blue">false</span>;
    }
    <span style="color: blue">else</span>
    {
        box::stop(strFmt(<span style="color: red">'%1\n\n%2\n%3\n\n%4\n%5'</span>,
            <span style="color: red">'Невозможно создать журнал - не указан филиал текущего пользователя!'</span>,
            <span style="color: red">'Пожалуйста, укажите филиал в поле &quot;Первичная группа пользователя&quot; формы &quot;Параметры&quot;,'</span>,
            <span style="color: red">'после чего начните создание журнала заново.'</span>,
            <span style="color: red">'Если Вы работаете в форме-списке журналов, то удалите неправильную запись без филиала'</span>,
            <span style="color: red">'или закройте и снова откройте форму.'</span>));

        <span style="color: blue">switch</span> (_common.TableId)
        {
            <span style="color: blue">case</span> <span style="color: blue">tableNum</span>(LedgerJournalTable):
                _common.(<span style="color: blue">fieldNum</span>(LedgerJournalTable, Name)) = #alarmJournalDescr;
                <span style="color: blue">break</span>;

            <span style="color: blue">case</span> <span style="color: blue">tableNum</span>(InventJournalTable):
                _common.(<span style="color: blue">fieldNum</span>(InventJournalTable, Description)) = #alarmJournalDescr;
                <span style="color: blue">break</span>;
        }
        <span style="color: blue">return</span> <span style="color: blue">true</span>; <span style="color: green">// Неизвестный филиал пользователя для создания журнала
</span>    }
}</pre></div>Получилось, конечно, несколько громоздковато, но, с другой стороны, производимый эффект совпадает с ожиданиями заказчика :)<br />
<br />
Происходит следующее. В зависимости от конкретной формы, для которой срабатывает insert, пользователю &quot;без филиала&quot; разрешается создать &quot;запись&quot; в гриде формы, и внешне она выглядит как обычная запись, до тех пор пока пользователь не перейдет на другую запись или не попробует перейти в строки журнала при помощи соответствующей кнопки. Тут и вылетает предупреждение, а поле описания журнала заполняется просьбой об удалении записи. Если пользователь закроет форму и потом откроет ее, то &quot;записи&quot; этой уже не будет.<br />
<br />
Буду рад, если кто-нибудь из читателей укажет путь к какому-нибудь более правильному методу решения этой задачи. Я почти уверен, что он существует! :)</div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=10</guid>
		</item>
		<item>
			<title>Явная невозможност</title>
			<link>//axforum.info/forums/blog.php?b=9</link>
			<pubDate>Tue, 28 Jul 2009 06:14:19 GMT</pubDate>
			<description><![CDATA[Хотел запоститься в блог с заголовком "Невозможность создавать складской журнал", а оно обрезается до "Невозможност" :(

В тоже время словосочетание "Возможность создавать складской журнал" полностью сохраняется как заголовок.]]></description>
			<content:encoded><![CDATA[<div>Хотел запоститься в блог с заголовком &quot;Невозможность создавать складской журнал&quot;, а оно обрезается до &quot;Невозможност&quot; :(<br />
<br />
В тоже время словосочетание &quot;Возможность создавать складской журнал&quot; полностью сохраняется как заголовок.</div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=9</guid>
		</item>
		<item>
			<title>Оставьте комментарии к этому посту</title>
			<link>//axforum.info/forums/blog.php?b=3</link>
			<pubDate>Tue, 14 Jul 2009 09:37:08 GMT</pubDate>
			<description>Коллеги, функционал блогов находится в стадии обкатки. 

Сейчас хочется посмотреть, как будут выглядеть комментарии к произвольному сообщению. Не стесняйтесь, пишите что-нибудь (хоть про погоду! :) ) 

Спасибо.</description>
			<content:encoded><![CDATA[<div>Коллеги, функционал блогов находится в стадии обкатки. <br />
<br />
Сейчас хочется посмотреть, как будут выглядеть комментарии к произвольному сообщению. Не стесняйтесь, пишите что-нибудь (хоть про погоду! :) ) <br />
<br />
Спасибо.</div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=3</guid>
		</item>
		<item>
			<title>Возможности блога на AxForum</title>
			<link>//axforum.info/forums/blog.php?b=2</link>
			<pubDate>Tue, 14 Jul 2009 06:36:12 GMT</pubDate>
			<description><![CDATA[Итак, в нашем распоряжении новая фича - блоги внутри Форума. Попробуем понять, как пользоваться этим функционалом.

Первое, что понравилось - есть возможность сохранения черновика сообщения. Т.е. можно набрасывать тезисы по темке, никому не показывая и не стесняясь, а потом однажды всё это более-менее оформить в связный текст и предъявить общественности.

Новое сообщение сохраняется как черновик путем нажатия на кнопку "Сохранить как черновик" (справа под окном редактирования текста). Последующий перевод черновика в нормальное сообщение осуществляется изменением статуса записи в блоке "Дополнительные опции" (и кнопка "Сохранить изменения")]]></description>
			<content:encoded><![CDATA[<div>Итак, в нашем распоряжении новая фича - блоги внутри Форума. Попробуем понять, как пользоваться этим функционалом.<br />
<br />
Первое, что понравилось - есть возможность сохранения черновика сообщения. Т.е. можно набрасывать тезисы по темке, никому не показывая и не стесняясь, а потом однажды всё это более-менее оформить в связный текст и предъявить общественности.<br />
<br />
Новое сообщение сохраняется как черновик путем нажатия на кнопку &quot;Сохранить как черновик&quot; (справа под окном редактирования текста). Последующий перевод черновика в нормальное сообщение осуществляется изменением статуса записи в блоке &quot;Дополнительные опции&quot; (и кнопка &quot;Сохранить изменения&quot;)</div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=2</guid>
		</item>
		<item>
			<title>Тест 1</title>
			<link>//axforum.info/forums/blog.php?b=1</link>
			<pubDate>Fri, 10 Jul 2009 06:50:57 GMT</pubDate>
			<description>Тест! А что же это такое?</description>
			<content:encoded><![CDATA[<div>Тест! А что же это такое?</div>

]]></content:encoded>
			<dc:creator>Gustav</dc:creator>
			<guid isPermaLink="true">//axforum.info/forums/blog.php?b=1</guid>
		</item>
	</channel>
</rss>
