Return True

03. Transitive

function transitive(x, y, z) {
  return x && x == y && y == z && x != z;
}

Функция принимает три параметра, при этом:

Иными словами, нам нужно найти три таких значения, для которых отношение равенства не транзитивно, и при этом первый из них — истинен.

Первым делом нужно вспомнить, как работает нестрогое сравнение.

Если имеем x == y, то алгоритм примерно следующий:

  1. Если типы x и y совпадают, то возвращаем результат строгого сравнения. То есть, сравниваем их значения (или ссылки, если это объекты). Единственная особенная ситуация тут — NaN, который не равен сам себе.

  2. Если один из операндов null, а второй undefined, возвращаем true.

  3. Если один из операндов типа String, а второй — Number, то приводим String к Number, а затем переходим к п. 1.

  4. Если один из операндов типа Boolean, приводим его к Number.

  5. Если один из операндов типа Object, приводим его к примитиву и начинаем сравнение заново, с п. 1.

  6. Иначе возвращаем false.

В реальности алгоритм чуть сложнее, и там ещё есть обработка BigInt, но нам это пока не принципиально.

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

Единственная «скользкая» ситуация тут — это тип Number с его NaN. Но он не может быть x, ибо не положительный. И не может быть y или z, т. к. над ними выполняется операция равенства, а NaN не равен ничему.

Выходит, типы разные, и нас интересует их приведение.


Учитывая тот факт, что мы имеем x == y и y == z, но при этом x != z, можно предположить, что y — это какой-то примитив, а x и z — объекты. Потому что объекты сравниваются по ссылке, и если ссылка разная, то они не равны друг другу. Это могут быть даже одинаковые по содержанию объекты. Ссылка-то у них будет разной.

Плюс это укладывается в то, что x должен быть истинным. Объекты ведь всегда истинны. А раз так, то можем использовать это знание для поиска y.

Например, банально: x и z — пустые объекты, а y — true. Пробуем:

> transitive({}, true, {})
false

Странно. Если начнём проверять пошагово, то окажется, что:

> ({}) == true
false

Как так? Мы ведь знаем, что в условии пустой объект ведёт себя как истинное значение:

> if ({}) console.log('True!')
True!

Да и первую проверку на истинность x пустой объект проходит. В чём же дело?

А дело в том, что согласно алгоритму нестрогого сравнения, что мы обсуждали ранее, если один из операндов типа Boolean, то он будет приведён к Number. И наш true после приведения к Number превращается в 1, конечно же. И при сравнении 1 с {} всё ломается.

Окей, тогда посмотрим, как приводятся объекты к примитивам. Это то, что нам пока не понятно из алгоритма сравнения.


Опуская детали, в нашем случае приведение объекта к примитиву выглядит так:

  1. Если у объекта есть метод, который в доке называется @@toPrimitive, то:

    1. Вызываем его.

    2. Если вернулся не объект, возвращаем это значение.

    3. Иначе выбрасываем ошибку.

  2. Если такого метода нет, то проверяем наличие метода valueOf:

    1. Если есть, то вызываем его.

    2. Если вернулся не объект, возвращаем результат.

  3. Если valueOf нет, проверяем аналогичным образом toString.

  4. Если ничего не сработало, выбрасываем ошибку.

@@toPrimitive — это на деле Symbol.toPrimitive, и такой метод есть только у двух типов: Date и Symbol.

Для Symbol возвращается сам символ:

> Symbol('asd')[Symbol.toPrimitive]()
Symbol(asd)

Тут важно подчеркнуть, что это не строка. Возвращается именно тип Symbol:

> typeof Symbol('asd')[Symbol.toPrimitive]()
"symbol"

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

А вот имплементация @@toPrimitive для Date такова, что там используется тот же алгоритм, что описан выше, только вызываемые методы поменяны местами: сперва тестируется toString, а потом valueOf. Получается, что решение с Date может быть таким:

transitive(new Date(), '<current date string>', new Date()))

Где <current date string> — текущее строковое представление даты. Можно попробовать использовать это знание, и с его помощью решить задачу. В этом случае решение будет похоже на эпизод из фильма вроде «Миссия невыполнима». У нас есть ровно секунда, чтобы оно сработало.

В моём случае получилось вот такое решение:

transitive(new Date(),'Sun Nov 22 2020 23:01:18 GMT+0200 (Eastern European Standard Time)',new Date())

Было сложно, да. Зато, вуа-ля! Мы нашли решение в 90 символов. Правда, судя по таблице рекордов, есть решение аж в 7!

Топ по задаче transitive

Мы, конечно, можем вызывать конструктор без ключевого слова new, и это сэкономит нам 8 символов. А ещё можем подобрать таймзону таким образом, чтобы её текстовое представление было покороче. Но всё это точно не приблизит нас к 7 символам. А значит, думаем дальше.


Есть ещё много разных объектов, но для начала стоит протестировать самые базовые: Object и Array. Нас интересует, что возвращают их методы valueOf и toString.

valueOf массива возвращает сам массив:

> [].valueOf()
[]

> [1].valueOf()
[1]

> [1, 2, 3].valueOf()
[1, 2, 3]

А toString возвращает что-то похожее на вызов метода .join():

> [].toString()
""

> [1].toString()
"1"

> [1, 2, 3].toString()
"1,2,3"

У объекта valueOf тоже возвращает сам объект:

> ({}).valueOf()
{}

> ({ a: 1 }).valueOf()
{ a: 1 }

А вот toString возвращает одну и ту же строку:

> ({}).toString()
"[object Object]"

> ({ a: 1 }).toString()
"[object Object]"

Согласно алгоритму, что мы обсуждали раньше, если valueOf возвращает объект, то тестируется toString. А это как раз наша ситуация. Можем как-то использовать toString объекта и массива.

Например, раз у нас для Object всегда возвращается одна и та же строка, мы вполне можем это использовать в качестве решения:

transitive({},'[object Object]',{})

23 символа! Всё ближе к победе!

Однако, интереснее всего в проверках себя повёл массив. Раз для него toString работает аналогично вызову .join(), то вариант с пустым массивом мы вполне можем использовать:

transitive([],'',[])

8 символов! Осталось понять, как сократить ещё один. Для этого надо вспомнить ещё раз в алгоритм проверки на равенство.


Если есть выражение [] == '', то логика вычисления значения следующая:

  1. Типы не совпадают.

  2. Операнды не null и не undefined.

  3. Операнды не String и Number.

  4. Операндов типа Boolean нет.

  5. Один операнд объект ([]). Приводим его к примитиву, получаем пустую строку (''). Начинаем заново.

  6. Типы совпадают, сравниваем строки как строки.

  7. Строки совпадают.

Поскольку мы тут ничего не можем сделать с массивом (кроме как изменить его, но пока не будем), то можно предположить, что второй операнд не строка, и посмотреть, что было бы дальше в таком случае.

  1. Тогда после приведения объекта к строке типы бы не совпадали.

  2. Операнды всё ещё не null и не undefined.

  3. Операнды всё ещё не String и Number... о!

У нас один операнд точно строка, т. к. массив в неё превратился. А второй вполне себе может быть числом!

Если так, то:

  1. Операнды String и Number. Приводим операнд типа String к типу Number и начинаем сравнение сначала.

  2. Типы совпадают, сравниваем числа как числа.

Если мы приведём пустую строку к числу, то получим ноль. А раз так, то вот наше решение в семь символов:

transitive([],0,[])