Форум программистов «Весельчак У»
  *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: Иерархический классификатор  (Прочитано 15753 раз)
0 Пользователей и 1 Гость смотрят эту тему.
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« : 10-10-2008 19:59 » 

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

(B(A(C, B), P(L, C, R, P, M, W, T)), O(I(D), C(N, O)))

Соответственно, имеем допустимые классификационные коды:

BAC, BAB, BPL, BPC, BPR, BPP, BPM, BPW, BPT, OID, OCN, OCO.

Если буквы в программе заменить на осмысленные названия, то получим набор enum-ов.

Спрашивается, как написать такой(ие) класс(ы) в программе (а точнее, какой у него(них) должен быть интерфейс), чтобы он(и):
1) описывал(и) значение классификатора;
2) позволял(и) бы пользователю читать отдельные позиции классификационного кода (поскольку они имеют самостоятельный смысл);
3) позволял(и) бы конструировать классификационный код в рамках допустимых классификатором возможностей - записывать отдельные позиции классификационного кода.

Можно, конечно, по позициям классификационного кода завести 3 enum-а:
1) B, O
2) A, P, I, C
3) C, B, L, C (2-й раз повторяется, можно убрать), R, P, M, W, T, D, N, O
и дальше на уровне логики записи блокировать присваивание недопустимых для 1 и 2-го уровней значений 3-го уровня и т.д.

Но можно ли изящнее решить задачу?

P.S. Прошу предлагать решения для языков программирования со строгой типизацией. Понятно, что в языках без строгой типизации можно менять интерфейс объекта во время исполнения.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Finch
Спокойный
Администратор

il
Offline Offline
Пол: Мужской
Пролетал мимо


« Ответ #1 : 10-10-2008 20:06 » 

dimka, А можно более наглядно и на пальцах объяснить? На примере. Что то в абстракции у меня туго. Не могу понять задачу.
Записан

Не будите спашяго дракона.
             Джаффар (Коша)
Finch
Спокойный
Администратор

il
Offline Offline
Пол: Мужской
Пролетал мимо


« Ответ #2 : 10-10-2008 20:23 » 

Из того, что я понял. Класс в принципе может быть один. Но он должен сам уметь выстраивать иеархические деревья. Теперь задача другая. Например B это просто буква, или это может быть и слово?
Записан

Не будите спашяго дракона.
             Джаффар (Коша)
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #3 : 10-10-2008 21:39 » 

Цитата: Finch
Например B это просто буква, или это может быть и слово?
Например, значение enum-а. При этом тип enum-а желательно сохранить.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Finch
Спокойный
Администратор

il
Offline Offline
Пол: Мужской
Пролетал мимо


« Ответ #4 : 10-10-2008 21:48 » 

На стадии компиляции это шаблоны. Но я так понял, тебе уже нужно на стадии работы программы?
Записан

Не будите спашяго дракона.
             Джаффар (Коша)
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #5 : 10-10-2008 21:55 » 

Finch, да. Видимо, нужно решение в духе объявления типов Variant.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Finch
Спокойный
Администратор

il
Offline Offline
Пол: Мужской
Пролетал мимо


« Ответ #6 : 10-10-2008 21:59 » 

Тут LogRus как-то в Эврике свой пример с enum давал. Можно сделать абстрактный базовый enum класс, и от него уже плодить потомков. Твоя Иеархия будет работать как с абстрактным классом, Но подставлять ты будеш потомков.

Теперь встает вопрос один. Как сделать так, чтобы класс знал, какие и сколько членов в enum. Ясно, что он будет работать как с числами. Но все равно, можно задать константы в разнобой.
« Последнее редактирование: 10-10-2008 22:04 от Finch » Записан

Не будите спашяго дракона.
             Джаффар (Коша)
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #7 : 11-10-2008 08:36 » 

Цитата: Finch
Тут LogRus как-то в Эврике свой пример с enum давал. Можно сделать абстрактный базовый enum класс, и от него уже плодить потомков. Твоя Иеархия будет работать как с абстрактным классом, Но подставлять ты будеш потомков.
Ни в коем случае. Будет в духе:

Код: (C++)
class MyEnum
 {
  public:
   enum Enum
    {
     A, B
    };
  private:
   Enum value;
  public:
   MyEnum(Enum value);
   Enum getValue() const;
   void setValue(Enum value);
 };
Такой класс имеет методы, типы результатов и аргументов которого привязаны к конкретному enum-у. Если мы вынесем enum в шаблонный параметр класса - ничего по сути не изменится, поскольку MyEnum<EnumA> и MyEnum<EnumB> - это разные типы с разными интерфейсами, и они не сводимы к общему классу с единым интерфейсом.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #8 : 11-10-2008 12:49 » 

Если писать на языке со строгой типизацией без применения параметризации классов, а также без RTTI и всяких хакерских приёмов, то получится примерно так:

Модуль классификатора:
Код: (Text) Turbo Pascal
{ Прототип реализации иерархического классификатора вида (A(C D) B(E F)). }
unit Classifier;

 interface

  type
   EnumType = (A, B);
   EnumAType = (C, D);
   EnumBType = (E, F);

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

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

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

   ClassifierLevelPointerType = ^ClassifierLevelType;
   ClassifierLevelType =
    object
     private
      HasValue: Boolean;
      NextLevelPointer: ClassifierLevelPointerType;
      constructor Create;
      procedure SetNextLevel(ANextLevel: ClassifierLevelPointerType);
      function Verify: Boolean; virtual;
      function NextConformsToCurrent: Boolean;
     public
      procedure Clear;
      function IsEmpty: Boolean;
    end;

   ClassifierLevel1PointerType = ^ClassifierLevel1Type;
   ClassifierLevel1Type =
    object(ClassifierLevelType)
     private
      Value: EnumType;
     public
      constructor Create;
      function SetValue(AValue: EnumType): Boolean;
      function GetValue(var AValue: EnumType): Boolean;
    end;

   ClassifierLevel2PointerType = ^ClassifierLevel2Type;
   ClassifierLevel2Type =
    object(ClassifierLevelType)
     private
      PreviousLevelPointer: ClassifierLevel1PointerType;
      ValueIn: Pointer;
      ValueA: EnumAType;
      ValueB: EnumBType;
      function Verify: Boolean; virtual;
      function PreviousConformsToCurrent(TestPreviousValue: EnumType): Boolean;
     public
      constructor Create(APreviousLevelPointer: ClassifierLevel1PointerType);
      function SetValueA(AValue: EnumAType): Boolean;
      function SetValueB(AValue: EnumBType): Boolean;
      function GetValueA(var AValue: EnumAType): Boolean;
      function GetValueB(var AValue: EnumBType): Boolean;
    end;

   ClassifierType =
    object
     private
      Level1: ClassifierLevel1Type;
      Level2: ClassifierLevel2Type;
     public
      constructor Create;
      function GetLevel1Pointer: ClassifierLevel1PointerType;
      function GetLevel2Pointer: ClassifierLevel2PointerType;
    end;

 implementation

  constructor ClassifierLevelType.Create;
   begin
    HasValue := False;
    NextLevelPointer := nil;
   end;

  procedure ClassifierLevelType.SetNextLevel;
   begin
    NextLevelPointer := ANextLevel;
   end;

  function ClassifierLevelType.Verify;
   begin
    Verify := True;
   end;

  function ClassifierLevelType.NextConformsToCurrent: Boolean;
   begin    
    if NextLevelPointer <> nil then
      NextConformsToCurrent := NextLevelPointer^.Verify
    else
      NextConformsToCurrent := True;
   end;

  procedure ClassifierLevelType.Clear;
   begin
    HasValue := False;
   end;

  function ClassifierLevelType.IsEmpty;
   begin
    IsEmpty := not HasValue;
   end;

  constructor ClassifierLevel1Type.Create;
   begin
    inherited Create;
   end;

  function ClassifierLevel1Type.SetValue;
   var
    OldHasValue: Boolean;
    OldValue: EnumType;
   begin
    OldValue := Value;
    OldHasValue := HasValue;
    Value := AValue;
    HasValue := True;
    if not NextConformsToCurrent then
     begin
      SetValue := False;
      Value := OldValue;
      HasValue := OldHasValue;
     end
    else
      SetValue := True;
   end;

  function ClassifierLevel1Type.GetValue;
   begin
    if HasValue then
     begin
      AValue := Value;
      GetValue := True;
     end
    else
     GetValue := False;
   end;

  function ClassifierLevel2Type.PreviousConformsToCurrent;
   var
    PreviousValue: EnumType;
   begin
    if PreviousLevelPointer^.GetValue(PreviousValue) then
      PreviousConformsToCurrent := PreviousValue = TestPreviousValue
    else
      PreviousConformsToCurrent := True;
   end;

  constructor ClassifierLevel2Type.Create;
   begin
    inherited Create;
    if APreviousLevelPointer = nil then
      Fail;
    PreviousLevelPointer := APreviousLevelPointer;
    ValueIn := nil;
   end;

  function ClassifierLevel2Type.Verify;
   begin
    if not HasValue then
      Verify := True
    else if (ValueIn = @ValueA) and PreviousConformsToCurrent(A) then
      Verify := True
    else if (ValueIn = @ValueB) and PreviousConformsToCurrent(B) then
      Verify := True
    else
      Verify := False
   end;

  function ClassifierLevel2Type.SetValueA;
   var
    OldHasValue: Boolean;
    OldValueA: EnumAType;
    OldValueB: EnumBType;
    OldValueIn: Pointer;
   begin
    OldValueA := ValueA;
    OldValueB := ValueB;
    OldValueIn := ValueIn;
    OldHasValue := HasValue;
    ValueA := AValue;
    HasValue := True;
    ValueIn := @ValueA;
    if not PreviousConformsToCurrent(A) or not NextConformsToCurrent then
     begin
      SetValueA := False;
      ValueA := OldValueA;
      ValueB := OldValueB;
      ValueIn := OldValueIn;
      HasValue := OldHasValue;
     end
    else
      SetValueA := True;
   end;

  function ClassifierLevel2Type.SetValueB;
   var
    OldHasValue: Boolean;
    OldValueA: EnumAType;
    OldValueB: EnumBType;
    OldValueIn: Pointer;
   begin
    OldValueA := ValueA;
    OldValueB := ValueB;
    OldValueIn := ValueIn;
    OldHasValue := HasValue;
    ValueB := AValue;
    HasValue := True;
    ValueIn := @ValueB;
    if not PreviousConformsToCurrent(B) or not NextConformsToCurrent then
     begin
      SetValueB := False;
      ValueA := OldValueA;
      ValueB := OldValueB;
      ValueIn := OldValueIn;
      HasValue := OldHasValue;
     end
    else
      SetValueB := True;
   end;

  function ClassifierLevel2Type.GetValueA;
   begin
    if HasValue and (ValueIn = @ValueA) then
     begin
      AValue := ValueA;
      GetValueA := True;
     end
    else
     GetValueA := False;
   end;

  function ClassifierLevel2Type.GetValueB;
   begin
    if HasValue and (ValueIn = @ValueB) then
     begin
      AValue := ValueB;
      GetValueB := True;
     end
    else
     GetValueB := False;
   end;

  constructor ClassifierType.Create;
   begin
    Level1.Create;
    Level2.Create(@Level1);
    Level1.SetNextLevel(@Level2);
   end;

  function ClassifierType.GetLevel1Pointer;
   begin
    GetLevel1Pointer := @Level1;
   end;

  function ClassifierType.GetLevel2Pointer;
   begin
    GetLevel2Pointer := @Level2;
   end;

 end.

Тестовая программа:
Код: (Text) Turbo Pascal
{ Тестовая программа для прототипа реализации иерархического классификатора. }
program TestClassifier(Input, Output);

 uses Classifier;

 procedure WriteResult(Message: string; Result: Boolean);
  begin
   Write(Message, ' => ');
   if Result then
     WriteLn('Успешно.')
   else
     WriteLn('Неудачно.');
  end;

 var
  ClassifierInstance: ClassifierType;
  Value: EnumType;
  ValueA: EnumAType;
  ValueB: EnumBType;
 begin
  WriteLn('Тесты классификатора (A(C D) B(E F)).');
  ClassifierInstance.Create;
  WriteResult('Установить A в уровне 1',
   ClassifierInstance.GetLevel1Pointer^.SetValue(A));
  WriteResult('Уровень 1 имеет значение',
   not ClassifierInstance.GetLevel1Pointer^.IsEmpty);
  WriteResult('Прочитать из уровня 1 значение',
   ClassifierInstance.GetLevel1Pointer^.GetValue(Value));
  WriteResult('Значение в уровне 1 равно A',
   Value = A);
  WriteResult('Не установить F в уровне 2',
   not ClassifierInstance.GetLevel2Pointer^.SetValueB(F));
  WriteResult('Уровень 2 не имеет значения',
   ClassifierInstance.GetLevel2Pointer^.IsEmpty);
  WriteResult('Установить C в уровне 2',
   ClassifierInstance.GetLevel2Pointer^.SetValueA(C));
  WriteResult('Уровень 2 имеет значение',
   not ClassifierInstance.GetLevel2Pointer^.IsEmpty);
  WriteResult('Прочитать из уровня 2 значение типа A',
   ClassifierInstance.GetLevel2Pointer^.GetValueA(ValueA));
  WriteResult('Значение типа A в уровне 2 равно C',
   ValueA = C);
  WriteResult('Не прочитать из уровня 2 значение типа B',
   not ClassifierInstance.GetLevel2Pointer^.GetValueB(ValueB));
  WriteResult('Не установить B в уровне 1',
   not ClassifierInstance.GetLevel1Pointer^.SetValue(B));
  ClassifierInstance.GetLevel1Pointer^.Clear;
  WriteLn('Очистить уровень 1.');
  WriteResult('Уровень 1 не имеет значения',
   ClassifierInstance.GetLevel1Pointer^.IsEmpty);
  WriteResult('Не прочитать из уровня 1 значение',
   not ClassifierInstance.GetLevel1Pointer^.GetValue(Value));
  WriteResult('Установить F в уровне 2',
   ClassifierInstance.GetLevel2Pointer^.SetValueB(F));
  WriteResult('Не установить A в уровне 1',
   not ClassifierInstance.GetLevel1Pointer^.SetValue(A));
  ClassifierInstance.GetLevel2Pointer^.Clear;
  WriteLn('Очистить уровень 2.');
  WriteResult('Не прочитать из уровня 2 значение типа A',
   not ClassifierInstance.GetLevel2Pointer^.GetValueA(ValueA));
  WriteResult('Не прочитать из уровня 2 значение типа B',
   not ClassifierInstance.GetLevel2Pointer^.GetValueB(ValueB));
  WriteResult('Установить A в уровне 1',
   ClassifierInstance.GetLevel1Pointer^.SetValue(A));
 end.

Протокол тестирования:
Код:
Тесты классификатора (A(C D) B(E F)).
Установить A в уровне 1 => Успешно.
Уровень 1 имеет значение => Успешно.
Прочитать из уровня 1 значение => Успешно.
Значение в уровне 1 равно A => Успешно.
Не установить F в уровне 2 => Успешно.
Уровень 2 не имеет значения => Успешно.
Установить C в уровне 2 => Успешно.
Уровень 2 имеет значение => Успешно.
Прочитать из уровня 2 значение типа A => Успешно.
Значение типа A в уровне 2 равно C => Успешно.
Не прочитать из уровня 2 значение типа B => Успешно.
Не установить B в уровне 1 => Успешно.
Очистить уровень 1.
Уровень 1 не имеет значения => Успешно.
Не прочитать из уровня 1 значение => Успешно.
Установить F в уровне 2 => Успешно.
Не установить A в уровне 1 => Успешно.
Очистить уровень 2.
Не прочитать из уровня 2 значение типа A => Успешно.
Не прочитать из уровня 2 значение типа B => Успешно.
Установить A в уровне 1 => Успешно.

Оно хоть всё и логично, но, по-моему, весьма громоздко. И громоздко потому, что типы участвующих в операциях переменных жёстко определены во время компиляции.
« Последнее редактирование: 11-10-2008 12:56 от dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Finch
Спокойный
Администратор

il
Offline Offline
Пол: Мужской
Пролетал мимо


« Ответ #9 : 11-10-2008 14:29 » 

Я с Variant типом знаком по QVariant из библиотеки Qt. Судя по тому, что я видел в этом классе, он тоже знает все типы еше на стадии компиляции, и со всеми типами он умеет обрашаться. Теперь при, задании ему какого либо значения, тип  которого он не знает, вызовет у него панику, еше на стадии компиляции, так как, компилятор не может подобрать соответствующий метод обработки.
Записан

Не будите спашяго дракона.
             Джаффар (Коша)
zubr
Гость
« Ответ #10 : 12-10-2008 14:11 » 

Как вариант:
Класс, создающий древовидный список из классов-элементов, описывающих элемент классификатора.
Вот примерный интерфейс на объектном паскале:
Код:
PClassifierType = ^TClassifierType;
  TClassifierType = class(TObject)
  private
    FNext: PClassifierType;
    FPrev: PClassifierType;
    FParent: PClassifierType;
    FChild: PClassifierType;
    FData: Pointer;
    FName: string;
  public
    property Next: PClassifierType read FNext write FNext;
    property Prev: PClassifierType read FPrev write FPrev;
    property Parent: PClassifierType read FParent write FParent;
    property Child: PClassifierType read FChild write FChild;
    property Data: Pointer read FData write FData;
    property Name: string read FName write FName;

    constructor Create; overload;
    destructor Destroy; override;
  end;

  PClassifier = ^TClassifier;
  TClassifier = class(TObject)
  public
    //получение массива строк возможных классов (типа 'BAC', 'BAB', 'BPL') для конкретного класса
    function GetPossebleClassString(ClassifierType: TClassifierType;
                                      ClassifierList: TStrings): boolean;
    //создает новый классифиткатор на основе имеющегося
    function CreateNewClassifier(const ClassifierArray: Variant;
                                         LenArray: Integer): PClassifier; overload;
    //создает новый классифиткатор на основе имеющегося по строке
    function CreateNewClassifier(const ClassifierString: string): PClassifier; overload;
    constructor Create(const ClassifierArray: Variant; LenArray: Integer); overload;
    destructor Destroy; override;
  end;

Пример создания вышеуказанного класса:
Код:
var
  A, B, C, D: TClassifierType;
  Classifier: TClassifier;
  v: Variant;

begin
 A := TClassifierType.Create;
 A.Name := 'A';
 B := TClassifierType.Create;
 B.Name := 'B';
 C := TClassifierType.Create;
 C.Name := 'C';
 D := TClassifierType.Create;
 D.Name := 'D';

 v := VarArrayOf([Integer(B), '(', Integer(A), '(', Integer(C), Integer(B), ')', ')']);
 Classifier := TClassifier.Create(v, 8);
Записан
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #11 : 12-10-2008 17:44 » 

zubr, не вполне уловил интерфейс - т.е. как пользователю удобно работать с такими классами. Где и как задаются базовые константы, с которыми пользователь будет сравнивать части классификатора?

Вообще, последовательности констант в классификаторе образуют слова некоторого простого языка, а сам классификатор - его грамматика + средство контроля составления пользователем слов такого языка (синтаксический анализатор).
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
zubr
Гость
« Ответ #12 : 12-10-2008 19:01 » 

Цитата
1) описывал(и) значение классификатора;
constructor Create(const ClassifierArray: Variant; LenArray: Integer); overload; - здесь в конструкторе создается древовидный список описывающий древовидную структуру классификатора. Предварительно создается массив указателей на созданные элементы классификатора и скобки, обозначающие иерархию.
Цитата
2) позволял(и) бы пользователю читать отдельные позиции классификационного кода (поскольку они имеют самостоятельный смысл);
function GetPossebleClassString(ClassifierType: TClassifierType; ClassifierList: TStrings): boolean; - метод по объекту элемента (можно сделать по имени) возращает список всех возможных вариантов классификатора, начиная с указанного элемента, к примеру для объекта A - вернется список 'AC', 'AB'.
Цитата
позволял(и) бы конструировать классификационный код в рамках допустимых классификатором возможностей - записывать отдельные позиции классификационного кода.
function CreateNewClassifier(const ClassifierArray: Variant; LenArray: Integer): PClassifier; - создание нового древовидного списка, сохраняя структуру базового

З.Ы. Интерфейс приблизительный и требует доработки.
З.Ы.З.Ы. Может я не правильно понял задачу?
Записан
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #13 : 12-10-2008 19:58 » 

zubr, а зачем пользователю получать список всех возможных вариантов? Тогда уж проще взять их все и перечислить в enum-е...
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
zubr
Гость
« Ответ #14 : 12-10-2008 21:18 » new

dimka, ну пусть это будет промежуточный внутренний метод класса, а для сравнения с пользовательской частью классификатора можно будет сделать что то типа:
function GetPossebleClassString(const ClassifierName: string; ClassifierList: TStrings): boolean; - где ClassifierName - имя первого элемента
function CompareClassifier(const UserClassifier:string):boolean; - метод внутри которого будет вызван GetPossebleClassString и из полученного списка сравниваться с UserClassifier
Вариант перечисления в энуме не может работать в рантайм. В варианте же с древовидным списком можно классификатор формировать любой глубины вложенности, причем динамически.
Записан
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines