morpher.ru 8 (499) 647 86 54
nowhere@morpher.ru
 
 
Мой морфер

Склонение по падежам на C# / VB.NET

Morpher.dll – это .NET-библиотека, реализующая функции склонения по падежам, автоматического определения рода словосочетания и прописи чисел на русском языке. Данная страница посвящена в основном технической стороне использования библиотеки; лингвистическая сторона вопроса раскрыта на странице Описание технологии склонения.

Реализованные функции

Весь API библиотеки можно представить следующим кодом:

namespace Slepov.Russian.Morpher
{
    public class Склонятель 
    {
        public Склонятель (string key, TextReader userDict = null);

        public IСклоняемое Проанализировать (
            string словосочетание, 
            params Признак [] признаки);

        public string Пропись (int n, ref string unit, 
            Падеж падеж = Падеж.Именительный);
    }
}

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

  • Поддерживаемые платформы: .NET 2.0 – 4.6.1, Silverlight 5, Mono, .NET Standard 2.0
  • Код библиотеки написан полностью на C# и скомпилирован для платформы MSIL (AnyCPU).
  • Библиотека не имеет никаких внешних зависимостей, не имеет интерфейса пользователя и не требует прав доступа к диску или сети.
  • Все public методы библиотеки потокобезопасны, что особенно актуально при её использовании в серверных приложениях. Потокобезопасность достигается без использования блокировок.

Комплект поставки

Morpher.dll ~400K Сама библиотека.
Morpher.xml ~10K Файл XML-комментариев для отображения подсказок в Visual Studio.

Функция склонения по падежам и числам

public IСклоняемое Проанализировать (string словосочетание, params Признак [] признаки);

Параметры:

словосочетание – слово или словосочетание для склонения. Должно быть на русском языке, иначе функция выдает null.

признаки сообщают функции дополнительную информацию о склоняемом словосочетании: какого оно рода и к какой категории относится. Признаки указывать необязательно, но в некоторых случаях их указание помогает снять неоднозначность, которая иначе привела бы к неправильному склонению.

public enum Признак
{
    ФИО, 
    Нарицательное, 
    МужскойРод,
    ЖенскийРод,
    Одушевлённое,
    Неодушевлённое
}

Подробнее об использовании признаков

Возвращает IСклоняемое.

Использование функции склонения проще всего проиллюстрировать на примере. Вот пример кода на C# для формирования текста, в котором одно и то же ФИО фигурирует в различных падежах:

    class HelloIvanIvanych
    {
        static void Main ()
        {
            Склонятель склонятель = new Склонятель ();
 
            IСклоняемое получатель = склонятель.Проанализировать ("Иванов Иван Иваныч");
 
            string справка = @"
                             СПРАВКА
               Выдана {0} в том, что 
            {1} была получена справка
            в том, что {2} не нуждается 
            более в получении никаких справок.";
 
            System.Console.WriteLine (
                справка, 
                получатель.Дательный,
                получатель.Творительный,
                получатель.Именительный);
        }
    }

(Да, это C# – он допускает использование русских букв в именах переменных и классов.)

Программа выдает в консольное окно следующий текст:


                                 СПРАВКА
                   Выдана Иванову Ивану Иванычу в том, что
                Ивановым Иваном Иванычем была получена справка
                в том, что Иванов Иван Иваныч не нуждается
                более в получении никаких справок.
    

Склонение производится за три этапа:

  • Создание экземпляра класса Склонятель.
  • Вызов метода Проанализировать.
  • Чтение одного из свойств интерфейса IСклоняемое для получения формы нужного падежа.

Вы можете спросить, зачем такие сложности? Почему бы не сделать API в виде одной функции:

public static string Просклонять (string s, Падеж падеж);

Это сделано для повышения быстродействия. Первый этап – создание экземпляра класса Склонятель – включает в себя загрузку словаря в память и занимает 100-200 миллисекунд. Вроде бы немного, но помещать эту операцию в цикл не стоит.

Второй этап – анализ – относительно сложный процесс, который включает в себя поиск по словарю и анализ синтаксической структуры словосочетания. Результат этого анализа, объект IСклоняемое, используется на третьем, последнем, этапе для генерации форм различных падежей единственного и множественного числа. Если необходимо генерировать несколько падежных форм, как в примере выше, то время экономится, т.к. анализ проводится всего один раз.

Знание этих особенностей позволяет сделать программу молниеносной. Приведу пример. Для контроля качества склонения используется база тестов, где каждый тест состоит из трех частей:

  • пример словосочетания в именительном падеже
  • идентификатор падежа
  • «правильный ответ» – как это словосочетание должно выглядеть в данном падеже.

Все тесты были составлены вручную. Я постоянно экспериментирую с алгоритмом склонения и каждый раз, внося изменения в программу, прогоняю все тесты, чтобы быть уверенным, что качество склонения не ухудшилось. На данный момент (1 сентября 2011 г.) объем базы составляет более 18 тысяч тестов. Поэтому мне важно, чтобы тесты прогонялись как можно быстрее.

Теперь внимание, вопрос! Как вы думаете, сколько времени занимает прогон всех тестов? Подсказка Ответ

Свойства интерфейса IСклоняемое

Объект IСклоняемое представляет собой совокупность форм одного числа (единственного или множественного).

Название свойства Тип возвращаемого значения Описание
Именительный string Возвращает форму именительного падежа.
Родительный string Возвращает форму родительного падежа.
Дательный string Возвращает форму дательного падежа.
Винительный string Возвращает форму винительного падежа.
Творительный string Возвращает форму творительного падежа.
Предложный string Возвращает форму предложного падежа с предлогом О/ОБ/ОБО (о компьютере).
ПредложныйБезПредлога string Возвращает форму предложного падежа без предлога (компьютере).
Где string Возвращает форму, отвечающую на вопрос где?в России, во Франции, в Крыму, на Канарах. Предлог выбирается автоматически.
Куда string Возвращает форму, отвечающую на вопрос куда?в Россию, во Францию, в Крым, на Канары. Предлог выбирается автоматически.
Откуда string Возвращает форму, отвечающую на вопрос откуда?из России, из Франции, из Крыма, с Канар. Предлог выбирается автоматически.
РодЧисло РодЧисло Возвращает род и число словосочетания в виде enum РодЧисло {ЕдинМуж, ЕдинЖен, ЕдинСред, Множ}
Множественное IСклоняемое Возвращает объект IСклоняемое, представляющий набор форм множественного числа или null, если склоняемое уже во множественном числе.

Определение рода и числа

Пример вызова:

switch (склонятель.Проанализировать("Чулпан").РодЧисло)
{
    case РодЧисло.ЕдинМуж: return "мужской род";
    case РодЧисло.ЕдинЖен: return "женский род";
    case РодЧисло.ЕдинСред: return "средний род";
    case РодЧисло.Множ: return "множественное число";
} 

Идентификация фамилии, имени и отчества в строке

Эта функция позволяет выделить в строке имя, фамилию и отчество. Например:

[TestMethod]
public void Александр_Сергеевич_Пушкин()
{
    var склоняемое = склонятель.Проанализировать ("Александр Сергеевич Пушкин");

    Assert.AreEqual ("Пушкин",    склоняемое.ФИО.Фамилия);
    Assert.AreEqual ("Александр", склоняемое.ФИО.Имя);
    Assert.AreEqual ("Сергеевич", склоняемое.ФИО.Отчество);
}

Фамилия, имя и отчество могут располагаться в строке в любом (грамматически допустимом) порядке. Отдельные компоненты ФИО могут отсутствовать, например:

На входе Фамилия Имя Отчество
Александр Сергеевич   Александр Сергеевич
Валентина Маркова Маркова Валентина  
Михаил Богданович Барклай-де-Толли Барклай-де-Толли Михаил Богданович
АГАЕВ ФАРИД ЗАКИР ОГЛЫ АГАЕВ ФАРИД ЗАКИР ОГЛЫ

В случае, если входная строка не распознана как ФИО, свойство ФИО будет равно null.

Склонение прилагательных по родам

string ИзменитьРод (string прилагательное, РодЧисло родЧисло);
    

Функция принимает на вход прилагательное в форме именительного падежа мужского рода, а также род и число, в которые его необходимо поставить. Вот пример функции, которая согласует прилагательное с существительным:

string Согласовать (string прилагательное, string существительное)
{
    return склонятель.ИзменитьРод (прилагательное, 
           склонятель.Проанализировать (существительное).РодЧисло) + " " + существительное;
}

Для прилагательного острый и существительных нож, бритва, копье, ножницы функция выдаст соответственно:

  • острый нож
  • острая бритва
  • острое копье
  • острые ножницы

Функция возвращает null, если:

  • входная строка равна null;
  • входная строка не может быть распознана как одно полное прилагательное или причастие в форме мужского рода именительного падежа единственного числа.

Необходимо учитывать, что смысл прилагательного может меняться в зависимости от существительного. Так, острое зрение и острый вкус не такие же острые, как острый нож.

Функция прописи чисел в заданном падеже

string Пропись (decimal n, ref string unit, Падеж падеж = Падеж.Именительный);
    

Возвращает пропись числа n, например, «сто двадцать пять» или «три целых две десятых» в указанном падеже (онлайн-демо). Если параметр unit не является словом или словосочетанием на русском языке или слово не имеет нужной падежной формы, то функция возвращает null.

Параметры:

n – число, пропись которого строится. Есть также перегруженный вариант метода для типа int. Функция работает для любых значений типов int и decimal. Для отрицательных значений пропись начинается со слова «минус». Для 0.0 – «ноль».

unit – единица измерения – слово или словосочетание на русском языке в именительном падеже единственного числа: «рубль», «доллар США», «календарный день», «новое письмо» и т.п. Если слово употребляется только во множественном числе, то указывается форма именительного падежа множественного числа: «сутки», «брюки», «ножницы».

падеж – при указании падежа пропись и единица измерения ставятся в заданный падеж. Единица измерения (unit) склоняется в большинстве случаев, даже если указан именительный падеж, как того требуют правила русского языка: «один рубль», «три рубля», «пять рублей», «пяти рублям», «пяти рублей» и т.п. Результат возвращается через тот же параметр unit (для этого указан модификатор ref).

Пример 1. Сумма прописью

Существует множество вариантов написания «суммы прописью». Программист, приступающий к решению этой задачи, должен ответить для себя на множество вопросов:

  1. Названия денежных единиц писать полностью или сокращенно?
  2. Выводить копейки числом или прописью или и так, и так?
  3. Брать в скобки только пропись или все выражение, следующее за суммой цифрами?
  4. Писать пропись со строчной или заглавной буквы?
  5. Писать ли названия денежных единиц после всей суммы цифрами?
  6. Как отображать ноль копеек: 00, словом «ровно» или знаком =?
  7. Как отображать ноль рублей?

В результате число возможных вариантов может исчисляться сотнями. Вот неполный список примеров:

  1. 333,12 руб. (Триста тридцать три рубля 12 копеек)
  2. 333 р. 12 коп. (Триста тридцать три рубля 12 копеек)
  3. 333,12 (триста тридцать три рубля двенадцать копеек)
  4. 333,12 (триста тридцать три рубля 12 копеек)
  5. 333,12 (триста тридцать три) рубля 12 копеек
  6. 333 (триста тридцать три) руб. 12 коп.
  7. 333 (триста тридцать три) рубля 12 копеек
  8. 333 (триста тридцать три) рубля 12 (двенадцать) копеек
  9. 333,12 (триста тридцать три и 12/100) руб.

На практике выбор того или иного варианта прописи определяется видом документа, где она фигурирует, и нормами конкретного предприятия. То есть проблема эта не лингвистическая, а бюрократическая. Поэтому в библиотеке нет готовой функции формирования суммы прописью, но ниже дан пример одного из возможных вариантов. Морфер делает за вас трудную работу по склонению и формированию прописи отдельных чисел, а как расставлять скобки и точки – решать вам:

    class ДенежнаяЕдиница 
    {
        public string ПолноеНаименованиеЦелойЧасти {get; set;}
        public string ПолноеНаименованиеДробнойЧасти {get; set;}
        public string СокращенноеНаименованиеЦелойЧасти {get; set;}
        public string СокращенноеНаименованиеДробнойЧасти {get; set;}

        public string СуммаПрописью (decimal summa, Падеж падеж = Падеж.Именительный)
        {
            var sb = new StringBuilder ();

            sb.Append (summa.ToString ("N2"));
            sb.Append (' ');
            sb.Append (СокращенноеНаименованиеЦелойЧасти);
            sb.Append (' ');
            sb.Append ('(');
            int i = sb.Length; // запомнить позицию первой буквы прописи
            sb.Append (Прописью (Math.Floor (summa), ПолноеНаименованиеЦелойЧасти, падеж));
            sb.Append (' ');
            sb.Append (Цифрами (summa * 100 % 100, ПолноеНаименованиеДробнойЧасти, падеж));
            sb.Append (')');

            sb [i] = char.ToUpper (sb [i]); // сделать первую букву прописи заглавной

            return sb.ToString ();
        }

        static string Прописью (decimal n, string unit, Падеж падеж)
        {
            return склонятель.Пропись (n, ref unit, падеж) + " " + unit;
        } 

        static string Цифрами (decimal n, string unit, Падеж падеж)
        {
            склонятель.Пропись (n, ref unit, падеж);
            return n.ToString ("00") + " " + unit;
        } 

        static readonly Склонятель склонятель = new Склонятель ();
    }

Вот как этот класс можно использовать:

        var рубли = new ДенежнаяЕдиница {
            ПолноеНаименованиеЦелойЧасти = "рубль",
            ПолноеНаименованиеДробнойЧасти = "копейка",
            СокращенноеНаименованиеЦелойЧасти = "руб."
        };

        var евро = new ДенежнаяЕдиница {
            ПолноеНаименованиеЦелойЧасти = "евро",
            ПолноеНаименованиеДробнойЧасти = "цент",
            СокращенноеНаименованиеЦелойЧасти = "евро"
        };

        Console.WriteLine (рубли.СуммаПрописью (100m));
        Console.WriteLine (рубли.СуммаПрописью (300333.12m));
        Console.WriteLine (евро.СуммаПрописью (400444.44m));
        Console.WriteLine ("За вычетом " 
            + рубли.СуммаПрописью (333.12m, Падеж.Родительный));

В результате получим:

100,00 руб. (Сто рублей 00 копеек)
300 333,12 руб. (Триста тысяч триста тридцать три рубля 12 копеек)
400 444,44 евро (Четыреста тысяч четыреста сорок четыре евро 44 цента)
За вычетом 333,12 руб. (Трёхсот тридцати трёх рублей 12 копеек)

Пример 2. Цена договора с НДС

string ПрописьРуб (Падеж падеж, int n)
{
    string unit = "рубль";  
    return n + " (" + склонятель.Пропись (n, ref unit, падеж) + ") " + unit;
} 

int цена = 1800;
Console.Write ("Цена договора составляет {0}, ", 
        ПрописьРуб (Падеж.Винительный, цена));
Console.Write ("в том числе НДС в размере {0}.", 
        ПрописьРуб (Падеж.Родительный, (int) (цена * 0.18 / 1.18)));

// Цена договора составляет 1800 (одну тысячу восемьсот) рублей, 
// в том числе НДС в размере 274 (двухсот семидесяти четырёх) рублей.

Пример 3. Перечень товаров

var товары = new []
{
    new {Наименование = "шампур из нержавеющей стали", Количество = 12},
    new {Наименование = "решетка-гриль", Количество = 2},
    new {Наименование = "щипцы для углей", Количество = 1},
};

foreach (var товар in товары)
{
    string unit = товар.Наименование;

    Console.WriteLine ("{0,-30} {1,2} ({2})", 
        товар.Наименование, 
        товар.Количество, 
        склонятель.Пропись (товар.Количество, unit));
}

// шампур из нержавеющей стали    12 (двенадцать)
// решетка-гриль                   2 (две)
// щипцы для углей                 1 (одни)

Пример 4. Социальная сеть

string Новых (int n, string unit)
{
    unit = склонятель.ИзменитьРод ("новый ", склонятель.Проанализировать (unit).РодЧисло) + unit;
    склонятель.Пропись (n, ref unit);
    return n + " " + unit;
} 

Console.WriteLine ("У вас " + Новых (5, "друг"));      // «У вас 5 новых друзей»
Console.WriteLine ("У вас " + Новых (1, "сообщение")); // «У вас 1 новое сообщение»
Console.WriteLine ("У вас " + Новых (2, "подарок"));   // «У вас 2 новых подарка»

Пример 5. День рождения

string Исполнилось (string кто, int сколько)
{
    string unit = "год";
    склонятель.Пропись (сколько, ref unit);
    return склонятель.Проанализировать (кто).Дательный 
        + " " + сколько + " " + unit;
}

Console.WriteLine (Исполнилось ("Анечка", 2)); // «Анечке 2 года»
Console.WriteLine (Исполнилось ("Вовочка", 5)); // «Вовочке 5 лет»
Console.WriteLine (Исполнилось ("Иван Иванович", 51)); // «Ивану Ивановичу 51 год»
Console.WriteLine (Исполнилось ("мы", 10)); // «нам 10 лет»

Попробуйте функцию «Пропись» в действии на сайте summa-pro.ru.

Защита от несанкционированного использования

Часть сборки Morpher.dll зашифрована и для ее правильной работы необходим лицензионный ключ. Ключ передается в конструктор класса Склонятель:

var склонятель = new Склонятель ("bApmPNweXmg=");
    

Каждый покупатель получает свой собственный уникальный ключ. При получении обновлений библиотеки ключ сохраняется.

Пользовательский словарь

Пользовательский словарь служит для коррекции склонения отдельных словосочетаний.

Чтобы подключить словарь к библиотеке, нужно передать его в конструктор класса Склонятель:

var склонятель = new Склонятель ("fbvB3vWfnrXQ==", new StreamReader ("MyDict.xml"));

Вызывать StreamReader.Dispose необязательно — за вас это сделает библиотека.

IUserDictionary

Имеется также конструктор, принимающий пользовательский словарь в виде интерфейса IUserDictionary:

public Склонятель (string key, IUserDictionary userDict);
    

Интерфейс имеет один метод, который возвращает словарную статью по заданной форме именительного падежа единственного числа:

public interface IUserDictionary
{
    Entry Lookup (string nominativeSingular);
}

Пока этот интерфейс используется только в Локальном веб-сервисе для кэширования словаря средствами ASP.NET.

История изменений

Дата Версия  
15.06.2012 2.3.3 В функцию Пропись добавлен учет единиц измерения, не имеющих единственного числа: двое суток, одни ножницы и т.п.
30.09.2012 2.3.4 Учет аббревиатуры "и.о." (исполняющий обязанности), небольшое пополнение словаря.
4.12.2012 2.3.5 К сборке добавлен атрибут AllowPartiallyTrustedCallers для решения проблемы с использованием сборки в Reporting Services.
23.12.2012 2.3.6 Реализована защита от несанкционированного использования.
12.01.2013 2.3.7 Исправлена ошибка в склонении отчества Николаевна и подобных.
30.01.2013 2.3.8 Исправлено склонение словосочетаний "Краснодарский край и Республика Адыгея" и "Республика Саха (Якутия)".
07.02.2013 2.3.9 В словарь добавлены названия минералов: анортоминасрагрит, балгарит, блёдит, вантгоффит...
24.03.2013 2.4.0 Добавлена возможность корректировки склонения при помощи пользовательского словаря.
18.06.2013 2.5.0 Добавлен интерфейс IUserDictionary для работы с пользовательским словарем.
23.06.2013 2.5.1 Пополнение словаря: проверено склонение более 200 стран и исправлены все ошибки. Особую трудность составило сокращение о-в (остров). Теперь это сокращение склоняется корректно: о-в Святой Елены, о-ва Туркс и Кайкос.
30.06.2013 2.5.2 Исправлена ошибка в функции Пропись: для единиц измерения во множественном числе (сутки) в косвенных падежах функция возвращала null. Теперь склоняется корректно: в течение трёх суток. Добавлены юнит-тесты, чтобы не допустить повторения этой проблемы в будущем. Количество юнит-тестов для функции Пропись перевалило за 200.
2.08.2013 2.6.0 Функция Пропись теперь работает с дробными числами: 5,3% (пять целых три десятых процента). Кроме того, расширен диапазон допустимых чисел и теперь он покрывает все возможные значения типа decimal. В связи с этим упразднено свойство MaxDecimal.
4.08.2013 2.6.1 В словарь добавлено более 400 популярных английских имен: Крис, Джеймс, Гарри, Уэйн... Элизабет, Уитни... Это поможет программе более правильно склонять английские имена и определять их пол.
23.09.2013 2.6.2 Исправлена ошибка в прописи числа 0,2 в винительном падеже. Эта ошибка касалась только чисел 0,2, 0,002, 0,000002 и т.д. и проявлялась только в винительном падеже ("двух десятых" вместо "две десятых").
20.11.2013 2.7.0
  • Поддержка числительных с наращением (1-я Парковая улица, 5-е колесо).
  • Из входной строки теперь удаляются мягкие переносы (U+00AD).
  • Добавлен встроенный словарь исключений.
  • Небольшое пополнение основного словаря (около 30 слов).
18.02.2014 2.7.3 Улучшено распознавание ФИО для составных имен. Теперь входное словосочетание признается ФИО, если хотя бы один из разделенных дефисом компонентов известен как имя. В результате стали правильно склоняться такие имена как ГЕЙ-ЛЮССАК Жозеф-Луи, Кени Шарль-Андре, Гильермо Гомес-Пенья и другие.
5.07.2014 2.7.4 Для помидоров, баклажанов и подобных названий (с пометой ["двойка в кружке"] по Зализняку) в родительном множественного теперь выдается форма с окончанием -ов, а не нулевым.
17.08.2014 2.8.0 Исправлена ошибка в прописи чисел вида ... x1y abc, где x=1..9, y=1..4, abc - любые 3 цифры, а ... - любая последовательность цифр, в т.ч. нулевой длины. Пример числа: 311 000. Ошибка проявлялась только в родительном падеже (трёхсот одиннадцати ТЫСЯЧИ вместо трёхсот одиннадцати ТЫСЯЧ).
17.08.2014 2.8.1 Пополнение словаря программы за счет Словаря новых слов (более 7200 статей).
27.08.2014 2.9.0 Добавлена возможность указания одушевленности входного слова или словосочетания при помощи значений Признак.Одушевлённое и Признак.Неодушевлённое. Такая возможность нужна для различения слов, которые могут называть лиц или предметы: укупорщик, взбиватель, тестер.
17.09.2014 2.9.2
  • Исправлено склонение наименования Наконечник E-Cu M 6/0,6/D=6,0/25,0 и подобных.
  • Названия со словом "набережная" теперь имеют предлог "на" в местном падеже: на Крутицкой набережной.
  • Пополнение словаря (106 новых слов)
17.09.2014 2.10.0 Добавлена функция разбивки ФИО на составляющие.
29.10.2014 2.10.1 Пополнение словаря (109 новых слов). В частности, исправлено склонение распространенного в Казахстане имени Бахыт, которое бывает и у мужчин, и у женщин, например:
МужчиныЖенщины
Бахыт КилибаевБахыт Жуматова
Бахыт АрыстановБахыт Сыдзыкова
Бахыт КенжеевБахыт Бегенова
11.11.2014 2.10.2
  • Улучшено склонение и определение рода имен, записанных полностью заглавными буквами. Например, раньше неправильно определялся род в имени ЧЕРНЫШЕВА ЮЛИЯ ВЛАДИМИРОВА. Теперь определяется как женский, несмотря на опечатку в отчестве.
  • Устранено исключение, возникавшее для "слов" на "-й" или "-ь", например Нью-Й или ночь-ь.
7.12.2014 2.10.3 Пополнение словаря (601 новое слово, в основном географические названия).
17.01.2015 2.11.0 Добавлена функция ИзменитьРод.
23.08.2015 2.12.0 В предыдущих версиях было невозможно вызвать метод Пропись из Visual Basic. Теперь это ограничение устранено путем удаления перегрузок метода, где параметр unit фигурировал без модификатора ref.
04.06.2016 2.13.1 Пополнение словаря. В частности, исправлено склонение словосочетания ТК-10 СИДиСР МВД.
28.01.2017 2.13.3 Пополнение словаря. В частности, добавлены слова и сокращения: фальш-камера, кабель-канал, ИБП, УЗО и др.
05.02.2017 2.13.4 Исправлена ошибка в методе Пропись. Ошибка проявлялась только для слова год в винительном падеже при n=1.
12.02.2017 2.13.5 Ограничено слишком агрессивное исправление латиницы на кириллицу, пополнение словаря.
31.07.2017 2.14.0 Поддержка .NET Standard 2.0.
29.08.2017 2.14.1 Исправлено возвращение разбиение ФИО при добавлении ФИО в пользовательский словарь.
04.09.2017 2.14.2 Исправлено склонение "Член Правления, директор по операциям"



 

Библиотеки

Заметили опечатку? Выделите слово мышкой и нажмите Ctrl-Enter. Система Orphus

© Сергей Слепов, 2003 - 2017. Перепечатка материалов сайта только с разрешения автора.