Как определить «или» логически


36

Недавно я столкнулся с проблемой, которая требовала от меня определения логического оператора «ИЛИ» программно, но без использования самого оператора.

Я придумал вот что:

OR(arg1, arg2)
  if arg1 = True and arg2 = True
     return True

  else if arg1 = True and arg2 = False
     return True

  else if arg1 = False and arg2 = True
     return True

  else:
     return False

Правильна ли эта логика, или я что-то упустил?


10
@gnat: Чтобы быть справедливым, в таблице истинности перечислены выходные данные для каждой комбинации входных данных, а в статье Википедии дается описание функции. Я думаю, что OP действительно спрашивает, как определить логическое ИЛИ программно без использования самого оператора.
Blrfl

6
@ user3687688 Не могли бы вы уточнить примитивы, которые нам разрешено использовать?
fredoverflow

4
этот вопрос вызвал коллективный спазм микрооптимизации;)
Роб

8
Вы можете использовать троичный операторreturn arg1 ? arg1 : arg2;
Мэтью

4
Я должен знать, почему вам нужно переопределить orоператора.
Кайл Стрэнд

Ответы:


102

Я бы сказал, что это правильно, но не могли бы вы сократить это до чего-то такого?

or(arg1, arg2)
    if arg1 == true
        return true
    if arg2 == true
        return true

    return false

Поскольку вы делаете сравнение или сравнение, я не думаю, что вам действительно нужно проверять комбинацию. Это просто имеет значение, если один из них верен, чтобы вернуть истину. В противном случае мы хотим вернуть false.

Если вы ищете более короткую версию, которая менее многословна, это также будет работать:

or(arg1, arg2)
    if arg1
        return arg1
    return arg2

6
Вы также можете удалить «еще» в строке 4 (оставляя только if arg2 == true).
Доусон Тот

1
@DawsonToth Есть много разных способов, которыми вы можете крутить его, зависит от того, хотите ли вы быть многословным или сжатым. Я был бы счастлив с остальным, если бы это звучало так, как будто это вопрос с псевдокодом, так что я бы, вероятно, оставил это так для ясности. Очень верно, хотя!
Эллиот Блэкберн

@BlueHat Кажется немного непоследовательным в использовании else if, но не в конце else.
SBoss

1
@ Mehrdad Спасибо! Я включил старый ответ обратно только потому, что чувствую, что он немного более многословен, и объясняет решение чуть яснее. Но ваше решение намного меньше и выполняет ту же работу.
Эллиот Блэкберн

1
даже лучше (хуже):or(a, b): a ? a : b
сара

149

Вот решение без или, и, не, сравнений и логических литералов:

or(arg1, arg2)
  if arg1
    return arg1
  else
    return arg2

Это, вероятно, не становится намного более фундаментальным, чем это;)


32
+1 за чуть более короткий ответ, чем мой. Тем не менее, я бы соблазнился отбросить «остальное» так же просто ради элегантности.
Эллиот Блэкберн

10
@BlueHat Но тогда два возврата будут иметь отступы по-разному;)
fredoverflow

5
Я хотел бы получать EUR каждый раз, когда кто-то сравнивает что-то с trueили false.
JensG

1
@JensG Ну, как ты думаешь, откуда доход Билла Гейтса?
Кролтан

1
||В двух словах оператор JavaScript (при реализации на динамически типизированном языке).
носорог

108

Одна строка кода:

return not (not arg1 and not arg2)

Нет ветвления, нет ИЛИ.

На языке Си это будет:

return !(!arg1 && !arg2);

Это просто применение законов де Моргана :(A || B) == !(!A && !B)


6
Я думаю, что этот подход - лучшее решение, поскольку (на мой взгляд) if/elseконструкция такая же, как использование OR, только с другим именем.
Ник

2
Использование @Nick ifэквивалентно равенству. Обычно в машинном коде an ifреализуется в виде арифметики с последующим сравнением с нулем с помощью скачка.


1
Мне нравится этот подход, потому что он закорачивает короткие замыкания IFF and, обеспечивая согласованность между операторами.
Кайл Стрэнд

1
@ Снеговик Это правда. Я имел в виду, что это if (a) return true; else if (b) return true;кажется более или менее морально эквивалентным if (a OR b) return true;, но эта точка зрения вполне может быть открыта для обсуждения.
Ник

13

Если у вас есть только andи not, вы можете использовать закон Деморгана, чтобы перевернуть and:

if not (arg1 = False and arg2 = False)
  return True
else
  return False

... или (еще проще)

if arg1 = False and arg2 = False
  return false
else
  return true

...

И поскольку все мы, очевидно, зациклились на оптимизации чего-то, что почти всегда доступно как машинная инструкция, это сводится к:

return not(not arg1 and not arg2)

return arg1 ? true : arg2

и тд и тд и тп

Так как большинство языков предоставляют условные-и, коэффициенты - это оператор "и" в любом случае подразумевает переход.

...

Если все, что у вас есть nand(см. Википедию ):

вернуть nand (nand (arg1, arg1), nand (arg2, arg2))


7
Упростить:return not (not arg1 and not arg2)

@ Снеговик, ты действительно должен ответить на этот вопрос, чтобы я мог его высказать. Вы (в настоящее время) здесь единственный, кто не пошел с ветвлением.
Lawtonfogle

4
Собирался добавить решение NAND, но вы меня опередили. Все должно быть реализовано с точки зрения NAND.
Энди

2
@ Энди: На самом деле, все должно быть определено с точки зрения NOR. ;-)
Питер Гиркенс

1
Хорошая работа с чистым nandраствором.
AAT

13

Функции (ECMAScript)

Все, что вам нужно, это определения функций и вызовы функций. Вам не нужны никакие ветвления, условия, операторы или встроенные функции. Я продемонстрирую реализацию с использованием ECMAScript.

Во-первых, давайте определим две функции с именем trueи false. Мы можем определить их так, как хотим, они совершенно произвольны, но мы определим их совершенно особым образом, что имеет некоторые преимущества, как мы увидим позже:

const tru = (thn, _  ) => thn,
      fls = (_  , els) => els;

truэто функция с двумя параметрами, которая просто игнорирует второй аргумент и возвращает первый. flsтакже является функцией с двумя параметрами, которая просто игнорирует свой первый аргумент и возвращает второй.

Почему мы кодировали truи flsтак? Ну, таким образом, две функции не только представляют две концепции trueи false, нет, они также представляют концепцию «выбора», другими словами, они также являются if/ then/ elseвыражением! Мы оцениваем ifусловие и передаем ему thenблок и elseблок в качестве аргументов. Если условие оценивается как tru, оно возвращает thenблок, если оно оценивает fls, оно возвращает elseблок. Вот пример:

tru(23, 42);
// => 23

Это возвращается 23, и это:

fls(23, 42);
// => 42

возвращается 42, как и следовало ожидать.

Есть морщина, однако:

tru(console.log("then branch"), console.log("else branch"));
// then branch
// else branch

Это печатает оба then branch и else branch! Зачем?

Ну, он возвращает возвращаемое значение первого аргумента, но он оценивает оба аргумента, поскольку ECMAScript является строгим и всегда оценивает все аргументы функции перед вызовом функции. IOW: он оценивает первый аргумент console.log("then branch"), который просто возвращает, undefinedи имеет побочный эффект печати then branchна консоль, и он оценивает второй аргумент, который также возвращает undefinedи печатает на консоль как побочный эффект. Затем возвращается первое undefined.

В λ-исчислении, где было изобретено это кодирование, это не проблема: λ-исчисление чисто , что означает, что оно не имеет побочных эффектов; поэтому вы никогда не заметите, что второй аргумент также оценивается. Кроме того, λ-исчисление является ленивым (или, по крайней мере, его часто оценивают в обычном порядке), то есть фактически не оценивает аргументы, которые не нужны. Итак, IOW: в λ-исчислении второй аргумент никогда не будет оцениваться, и если бы это было так, мы бы этого не заметили.

ECMAScript, однако, является строгим , то есть он всегда оценивает все аргументы. Ну, на самом деле, не всегда: if/ then/ else, например, только оценивает thenветвь, если условие есть, trueи только оценивает elseветвь, если условие есть false. И мы хотим повторить это поведение с нашими iff. К счастью, несмотря на то, что ECMAScript не ленив, у него есть способ отложить оценку фрагмента кода, точно так же, как это делает почти любой другой язык: обернуть его в функцию, и если вы никогда не вызовете эту функцию, код будет никогда не будет казнен.

Итак, мы заключаем оба блока в функцию, и в конце вызываем возвращаемую функцию:

tru(() => console.log("then branch"), () => console.log("else branch"))();
// then branch

печатает then branchи

fls(() => console.log("then branch"), () => console.log("else branch"))();
// else branch

отпечатки else branch.

Мы могли бы реализовать традиционный if/ then/ elseтаким образом:

const iff = (cnd, thn, els) => cnd(thn, els);

iff(tru, 23, 42);
// => 23

iff(fls, 23, 42);
// => 42

Опять же, нам нужна дополнительная упаковка функций при вызове iffфункции и дополнительные скобки вызова функции в определении iff, по той же причине, что и выше:

const iff = (cnd, thn, els) => cnd(thn, els)();

iff(tru, () => console.log("then branch"), () => console.log("else branch"));
// then branch

iff(fls, () => console.log("then branch"), () => console.log("else branch"));
// else branch

Теперь, когда у нас есть эти два определения, мы можем их реализовать or. Сначала мы смотрим на таблицу истинности or: если первый операнд является правдивым, то результат выражения совпадает с первым операндом. В противном случае, результат выражения является результатом второго операнда. Вкратце: если первый операнд true, мы возвращаем первый операнд, в противном случае мы возвращаем второй операнд:

const orr = (a, b) => iff(a, () => a, () => b);

Давайте проверим, что это работает:

orr(tru,tru);
// => tru(thn, _) {}

orr(tru,fls);
// => tru(thn, _) {}

orr(fls,tru);
// => tru(thn, _) {}

orr(fls,fls);
// => fls(_, els) {}

Большой! Однако это определение выглядит немного некрасиво. Помните, truи flsуже сами по себе действуйте как условные, так что на самом деле в этом нет необходимости iff, и, следовательно, все эти функции оборачиваются вообще:

const orr = (a, b) => a(a, b);

Там у вас есть это: or(плюс другие логические операторы) определены только с определениями функций и вызовами функций всего за несколько строк:

const tru = (thn, _  ) => thn,
      fls = (_  , els) => els,
      orr = (a  , b  ) => a(a, b),
      nnd = (a  , b  ) => a(b, a),
      ntt = a          => a(fls, tru),
      xor = (a  , b  ) => a(ntt(b), b),
      iff = (cnd, thn, els) => cnd(thn, els)();

К сожалению, эта реализация довольно бесполезна: в ECMAScript нет функций или операторов, которые возвращают truили fls, все они возвращают trueили false, поэтому мы не можем использовать их с нашими функциями. Но мы все еще можем многое сделать. Например, это реализация односвязного списка:

const cons = (hd, tl) => which => which(hd, tl),
      car  = l => l(tru),
      cdr  = l => l(fls);

Объекты (Скала)

Возможно, вы заметили что-то своеобразное: truи, flsиграя двойную роль, они действуют как значения данных trueи false, в то же время, они также действуют как условное выражение. Это данные и поведение , объединенные в один ... хм ... "предмет" ... или (смею сказать) объект !

Действительно, truи flsесть объекты. И, если вы когда-либо использовали Smalltalk, Self, Newspeak или другие объектно-ориентированные языки, вы заметите, что они реализуют булевы значения точно таким же образом. Я продемонстрирую такую ​​реализацию здесь, в Scala:

sealed abstract trait Buul {
  def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): T
  def &&&(other:Buul): Buul
  def |||(other:Buul): Buul
  def ntt: Buul
}

case object Tru extends Buul {
  override def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): U = thn
  override def &&&(other:Buul) = other
  override def |||(other:Buul): this.type = this
  override def ntt = Fls
}

case object Fls extends Buul {
  override def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): V = els
  override def &&&(other:Buul): this.type = this
  override def |||(other:Buul) = other
  override def ntt = Tru
}

object BuulExtension {
  import scala.language.implicitConversions
  implicit def boolean2Buul(b:Boolean) = if (b) Tru else Fls
}

import BuulExtension._

(2 < 3) { println("2 is less than 3") } { println("2 is greater than 3") }
// 2 is less than 3

Вот почему, кстати, всегда работает функция «Заменить условное на полиморфное рефакторинг»: вы всегда можете заменить любое условное выражение в вашей программе полиморфной отправкой сообщений, потому что, как мы только что показали, полиморфная диспетчеризация сообщений может заменить условные выражения, просто внедрив их. Такие языки, как Smalltalk, Self и Newspeak, являются доказательством существования, потому что у этих языков нет даже условных выражений. (У них также нет циклов, BTW или вообще каких- либо встроенных в язык структур управления, кроме полиморфной отправки сообщений, называемых вызовами виртуальных методов.)


Pattern Matching (Haskell)

Вы также можете определить, orиспользуя сопоставление с образцом или что-то вроде определения частичной функции Haskell:

True ||| _ = True
_    ||| b = b

Конечно, сопоставление с образцом является формой условного выполнения, но опять же, как и объектно-ориентированная отправка сообщений.


2
Как насчет False ||| False = Falseи _ ||| _ = Trueвместо? :)
fredoverflow

3
@FredOverflow: для этого всегда нужно вычислять правильный операнд. Обычно булевы операторы должны быть нестрогими в своем правильном аргументе, называемом «короткое замыкание».
Йорг Миттаг

Ах, конечно. Я знал, что должна быть более глубокая причина :)
fredoverflow

Первая часть сразу напомнила мне великолепную серию Эрика Липперта о продолжении стиля прохождения . Чисто случайно, но все еще весело :)
Воо

1
@ JörgWMittag Определение FredOverflow имеет короткое замыкание. Попробуйте True ||| undefinedсебя в ghci, чтобы увидеть!
Даниэль Вагнер

3

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

Это, конечно, довольно тривиально в языках более высокого уровня, таких как Javascript или Perl, но я пишу этот пример на C, чтобы показать, что техника не зависит от особенностей языка высокого уровня:

#include <stdio.h>

int main (void) {
    // Define truth table for OR:
    int OR[2][2] = {
        {0,   // false, false
         1},  // false, true
        {1,   // true, false
         1}   // true, true
    }

    // Let's test the definition
    printf("false || false = %d\n",OR[1==2]['b'=='a']);
    printf("true || false = %d\n",OR[10==10]['b'=='a']);

    // Usage:
    if (OR[ 1==2 ][ 3==4 ]) {
        printf("at least one is true\n");
    }
    else {
        printf("both are false\n");
    }
}

Вы можете сделать то же самое с AND, NOR, NAND, NOT и XOR. Код достаточно чистый, чтобы выглядеть как синтаксис, так что вы можете делать что-то вроде этого:

if (OR[ a ][ AND[ b ][ c ] ]) { /* ... */ }

Я думаю, что это «самый чистый» подход в определенном математическом смысле. Оператор OR - это, в конце концов, функция, и таблица истинности является сущностью этой функции как отношения и множества. Конечно, это может быть написано в забавной ОО-манере тоже:BinaryOperator or = new TruthTableBasedBinaryOperator(new TruthTable(false, true, true, true));
ПРИХОДИТ С

3

Еще один способ выразить логические операторы в виде целочисленных арифметических выражений (где это возможно). Таким образом можно избежать большого количества ветвлений для большего выражения многих предикатов.

Пусть True будет 1 Пусть False будет 0

если сумма обоих значений больше 1, то это будет true или false, чтобы быть возвращенным.

boolean isOR(boolean arg1, boolean arg2){

   int L = arg1 ? 1 : 0;
   int R = arg2 ? 1 : 0;

   return (L+R) > 0;

}

6
booleanExpression ? true : falseтривиально равно booleanExpression.
Кин

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

1
return (arga+argb)>0
Грант

1
Я только исправлял твой текст. Ваш код идеален, но может быть в одной строке: return (((arg1 ? 1 : 0)+(arg2 ? 1 : 0)) > 0); :)
Grantly

1
@SenthuSivasambu Я не возражаю против вашего использования arg1 ? 1 : 0;. Это надежные выражения для преобразования логического числа в число. Это только оператор возврата, который может быть тривиально реорганизован.
Кин

1

Две формы:

OR(arg1, arg2)
  if arg1
     return True
  else:
     return arg2

ИЛИ

OR(arg1, arg2)
  if arg1
     return arg1
  else:
     return arg2

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

Определение Javascript ||сродни этому, что в сочетании с его свободной типизацией означает, что выражение false || "abc"имеет значение "abc"и 42 || "abc"имеет значение 42.

Хотя, если у вас уже есть другие логические операторы, то подобное nand(not(arg1), not(arg2))может иметь преимущество в том, что ветвления вообще нет.


какой смысл повторять предыдущий ответ ( как вы признались )?
комнат

@gnat это достаточно близко, что я бы не стал беспокоиться, если бы видел этот ответ, но в нем все еще есть что-то, чего не было найдено ни в одном из них, поэтому я оставляю его.
Джон Ханна

@gnat, на самом деле учитывая «Мы ищем длинные ответы, которые дают некоторое объяснение и контекст». Теперь я счастлив с этим ответом.
Джон Ханна

1

В дополнение ко всем запрограммированным решениям, использующим конструкцию if, можно создать вентиль ИЛИ путем объединения трех вентилей NAND. Если вы хотите увидеть, как это делается в википедии, нажмите здесь .

Из этого выражения

НЕ [НЕ (А И А) И НЕ (Б И Б)]

который использует NOT и AND дает тот же ответ, что и OR. Обратите внимание, что использование как NOT, так и AND является просто неясным способом выражения NAND.


НЕ (А И А) == НЕ (А)?
Чарли

Да, точно. В той же статье в википедии вы можете увидеть, как они сокращают НЕ ворота к воротам NAND. То же самое для И-ворот. Я решил не редактировать формулу, которую они представили для ворот ИЛИ.
Уолтер Митти

1

Все хорошие ответы уже даны. Но я не позволю этому остановить меня.

// This will break when the arguments are additive inverses.
// It is "cleverness" like this that's behind all the most amazing program errors.
or(arg1, arg2)
    return arg1 + arg2
    // Or if you need explicit conversions:
    // return (bool)((short)arg1 + (short)arg2)

В качестве альтернативы:

// Since `0 > -1`, negative numbers will cause weirdness.
or(arg1, arg2)
    return max(arg1, arg2)

Я надеюсь, что никто никогда не будет использовать такие подходы. Они здесь только для повышения осведомленности об альтернативах.

Обновить:

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

or(arg1, arg2)
    return !(!arg1 * !arg2)

Это просто использует законы Деморгана и злоупотребляет тем, что *похоже на то, &&когда trueи с falseкоторыми обращаются как 1и 0соответственно. (Подожди, ты говоришь, что это не код гольф?)

Вот достойный ответ:

or(arg1, arg2)
    return arg1 ? arg1 : arg2

Но это по сути идентично другим ответам, которые уже даны.


3
Эти подходы в корне ошибочны. Рассмотрим -1 + 1 для arg1+arg2, -1 и 0 для max(arg1,arg2)и т. Д.
пушистый

@fluffy Этот подход предполагает логические аргументы, а затем он просто работает правильно с большинством видов ввода мусора. Хорошо, что вы отметили, что есть еще мусор, который создает проблемы. Именно такого рода вещи и должны быть направлены на то, чтобы на практике смоделировать реальную проблемную область как можно более непосредственно (и избегать увлечения нашей собственной сообразительностью).
Кин

Если вы делаете чистые 1-битные логические значения, то сложение все равно не работает, так как 1 + 1 = 0. :)
пушистый

@fluffy Вот где приходят явные преобразования. Будут они нужны или нет, зависит от деталей реализации (именно поэтому это глупая идея).
Кин

0

Один из способов определить orэто с помощью таблицы поиска. Мы можем сделать это явно:

bool Or( bool a, bool b } {
  bool retval[] = {b,true}; // or {b,a};
  return retval[a];
}

мы создаем массив со значениями, которые возвращаемое значение должно иметь в зависимости от того, что aесть. Тогда мы делаем поиск. В языках, подобных C ++, boolповышается до значения, которое можно использовать в качестве индекса массива с trueбытием 1и falseбытием 0.

Затем мы можем распространить это на другие логические операции:

bool And( bool a, bool b } {
  bool retval[] = {false,b}; // or {a,b};
  return retval[a];
}
bool Xor( bool a, bool b } {
  bool retval[] = {b,!b};
  return retval[a];
}

Недостатком всего этого является то, что требуется префиксная нотация.

namespace operators {
  namespace details {
    template<class T> struct is_operator {};
    template<class Lhs, Op> struct half_expression { Lhs&& lhs; };
    template<class Lhs, class Op>
    half_expression< Lhs, Op > operator*( Lhs&&lhs, is_operator<Op> ) {
      return {std::forward<Lhs>(lhs)};
    }
    template<class Lhs, class Op, class Rhs>
    auto operator*( half_expression<Lhs, Op>&& lhs, Rhs&& rhs ) {
    return invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) );
    }
  }
  using details::is_operator;
}

struct or_tag {};
static const operators::is_operator<or_tag> OR;

bool invoke( bool a, or_tag, bool b ) {
  bool retval[] = {b,true};
  return retval[a];
}

и теперь вы можете напечатать, true *OR* falseи это работает.

Приведенная выше методика требует языка, который поддерживает поиск, зависящий от аргументов, и шаблонов. Возможно, вы могли бы сделать это на языке с генериками и ADL.

Кроме того, вы можете расширить *OR*вышеперечисленное для работы с наборами. Просто создайте свободную функцию invokeв том же пространстве имен, что и or_tag:

template<class...Ts>
std::set<Ts...> invoke( std::set<Ts...> lhs, or_tag, std::set<Ts...> const& rhs ) {
  lhs.insert( rhs.begin(), rhs.end() );
  return lhs;
}

и теперь set *OR* setвозвращает союз двух.


0

Этот вспоминает мне характерные функции:

or(a, b)
    return a + b - a*b

Это относится только к языкам, которые могут трактовать логические значения как (1, 0). Не применяется к Smalltalk или Python, так как логический класс является классом. В smalltalk они идут еще дальше (это будет написано в виде псевдокода):

False::or(a)
    return a

True::or(a)
    return self

А двойные методы существуют для и:

False::and(a)
    return self

True::and(a)
    return a

Таким образом, «логика» совершенно верна в операторе OP, хотя и многословна. Осторожно, это не плохо. Идеально, если вам нужна функция, которая действует как математический оператор, например, на основе своего рода матрицы. Другие будут реализовывать фактический куб (например, оператор Quine-McCluskey):

or = array[2][2] {
    {0, 1},
    {1, 1}
}

И вы будете оценивать или [a] [b]

Так что да, каждая логика здесь действительна (кроме той, которая опубликована с использованием оператора ИЛИ языка xDDDDDDDD).

Но мой любимый закон Деморгана: !(!a && !b)


0

Посмотрите на стандартную библиотеку Swift и проверьте их реализацию операций быстрого доступа ИЛИ и быстрого вызова И, которые не оценивают вторые операнды, если они не нужны / не разрешены.


-2

Логика совершенно правильная, но ее можно упростить:

or(arg1, arg2)
  if arg1 = True
     return True
  else if arg2 = True
     return True
  else
     return False

И, вероятно, у вашего языка есть оператор ИЛИ, поэтому - если это не противоречит духу вопроса - почему бы и нет

or(arg1, arg2)
  if arg1 = True or arg2 = True
     return True
  else
     return False

if arg1 = True or arg2 = True { return true } else { return false }Еще лучше return arg1 = True or arg2 = True. if condition then true else falseизбыточно
Довал

4
Аскер специально указал, что их требование было «без использования самого оператора»
Гнат

2
Я не сказал ничего подобного. Это было что-то вроде того, что я имел в виду, но вопрос не говорил об этом до тех пор, пока он не был отредактирован, и она ответила на него как таковая, так что вроде как я в этом виноват.
logicNoob
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.