03. Transitive
function transitive(x, y, z) {
return x && x == y && y == z && x != z;
}
Функция принимает три параметра, при этом:
- первый параметр истинный;
- первый параметр нестрого равен второму;
- второй нестрого равен третьему;
- но первый нестрого не равен третьему.
Иными словами, нам нужно найти три таких значения, для которых отношение равенства не транзитивно, и при этом первый из них — истинен.
Первым делом нужно вспомнить, как работает нестрогое сравнение.
Если имеем x == y
, то алгоритм примерно следующий:
Если типы
x
иy
совпадают, то возвращаем результат строгого сравнения. То есть, сравниваем их значения (или ссылки, если это объекты). Единственная особенная ситуация тут — NaN, который не равен сам себе.Если один из операндов
null
, а второйundefined
, возвращаемtrue
.Если один из операндов типа String, а второй — Number, то приводим String к Number, а затем переходим к п. 1.
Если один из операндов типа Boolean, приводим его к Number.
Если один из операндов типа Object, приводим его к примитиву и начинаем сравнение заново, с п. 1.
Иначе возвращаем
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
с {}
всё ломается.
Окей, тогда посмотрим, как приводятся объекты к примитивам. Это то, что нам пока не понятно из алгоритма сравнения.
Опуская детали, в нашем случае приведение объекта к примитиву выглядит так:
Если у объекта есть метод, который в доке называется
@@toPrimitive
, то:Вызываем его.
Если вернулся не объект, возвращаем это значение.
Иначе выбрасываем ошибку.
Если такого метода нет, то проверяем наличие метода
valueOf
:Если есть, то вызываем его.
Если вернулся не объект, возвращаем результат.
Если
valueOf
нет, проверяем аналогичным образомtoString
.Если ничего не сработало, выбрасываем ошибку.
@@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!
Мы, конечно, можем вызывать конструктор без ключевого слова 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 символов! Осталось понять, как сократить ещё один. Для этого надо вспомнить ещё раз в алгоритм проверки на равенство.
Если есть выражение [] == ''
, то логика вычисления значения следующая:
Типы не совпадают.
Операнды не
null
и неundefined
.Операнды не String и Number.
Операндов типа Boolean нет.
Один операнд объект (
[]
). Приводим его к примитиву, получаем пустую строку (''
). Начинаем заново.Типы совпадают, сравниваем строки как строки.
Строки совпадают.
Поскольку мы тут ничего не можем сделать с массивом (кроме как изменить его, но пока не будем), то можно предположить, что второй операнд не строка, и посмотреть, что было бы дальше в таком случае.
Тогда после приведения объекта к строке типы бы не совпадали.
Операнды всё ещё не
null
и неundefined
.Операнды всё ещё не String и Number... о!
У нас один операнд точно строка, т. к. массив в неё превратился. А второй вполне себе может быть числом!
Если так, то:
Операнды String и Number. Приводим операнд типа String к типу Number и начинаем сравнение сначала.
Типы совпадают, сравниваем числа как числа.
Если мы приведём пустую строку к числу, то получим ноль. А раз так, то вот наше решение в семь символов:
transitive([],0,[])