Однострочный калькулятор, искусство или порок?

Вводная


Как это часто бывает, когда Вы ищете работу, Вы проходите одно собеседование за другим. Где-то Вас выбирают, где-то Вы. И наверное, в жизни каждого из нас бывали интересные собеседования, о которых можно с удовольствием поведать публике. Я хочу рассказать об одной такой истории, где есть место эмоциям, панике, потоку мышления и вдохновению. Речь в статье пойдет о внутренних переживаниях соискателя, о его противостоянии с интервьюером, интересный и мозговзрывательный код на java, а также ответ на поставленный вопрос: 'Необычный код — искусство или порок?'. Вы сможете окунуться в свое прошлое и размять мозги. Если заинтриговал, тогда поехали.

История одного человека X


В далеком 2008 году, парень по имени X искал работу программистом. Опыт разработки у него был, но не такой, когда отрывают с руками и ногами. Поэтому он отвлекался на все вакансии и отвечал на все звонки. И вот свершилось, X’а пригласили в серьезную компанию, где ему предстояло пройти всего 2 собеседования. Одно, как это водится, с девчатами из отдела кадров, которое впрочем помехой не стало, а второе — техническое, волнительное, сердце тревожное, неизвестное. Настал час X — собеседование. После принятого рукопожатия, молодой человек, по имени Y, интервьюер, истинный программист — прическа в бок, джинса подстёрлась, сказал, что очень занят сейчас. Ну раз ты пришел, так и быть, дам тебе хорошенький ноутбук и задачку — 'Напиши мне калькулятор. Простой калькулятор, когда на вход программе подается выражение, состоящее из 2 чисел, разделенные знаком '+', '-', '*', '/', которое нужно посчитать. У тебя полтора часа.'. И в тот момент, произошло нечто важное — 'Удиви меня!', надменно добавил он и ушел. В эту секунду человека X накрыл шквал негативных эмоций — 'Ага, ща. Достам АКС 74, 5.45 и заставлю тебя танцевать лезгинку и напевать Надежду Бабкину — 'Виновата ли я'. Во диву то будет, танцуй сколько хошь… '.
Но эмоции на то и эмоции, чтобы уступать место здравому смыслу. Грубость — не аргумент. Процесс пошел, мысли забурлили:, а может вызвать calc.exe, а может ООП навернуть, а может офигенный парсер выражения сделать. Но нет. Всего полтора часа. Может просто сделать задачу? Как поступить? Путь был выбран — 'Сделаю как смогу и точка с запятой. Ох уж и постановочка, ох уж и собеседование'. Минут через 20 на лице X’а появилась улыбка. Его осенило! А что если написать калькулятор, код которого содержал бы всего 1 строку, т.е. всего 1у точку с запятой не считая пакеты и импорты? Сказано — сделано. К концу второго часа решение было готово. 2 часа. Пришел уставший и немного замороченный Y. 'Ну как?' — спросил он X’а. 'Готово!' — ответил тот.

Интересный и мозговзрывательный код на java


Итак, дорогой читатель пришло время и нам с Вами попробовать решить поставленную, человеку X, задачку. Вот более точная формулировка задания: Необходимо написать калькулятор для простого выражения, который бы содержал ровно 1 строчку кода и умел складывать, вычитать, умножать и делить 2 числа. 1 строчка — означает ровно 1 точку с запятой, исключая декларацию пакета и импортов. Для решения можно использовать только классы из jdk. Примеры выражения »7 + 4»,»-12.0×14.12» без скобок и каких либо хитростей. Решение нужно оформить в 1 статическом методе, выводящего в консоль результат. Функцию main не трогать — в ней будут вызываться функции для проверки результатов. С ограничениями, пожалуй все. Любые трюки приветствуются. Оригинальность тоже.

Варианты


В java 7 это делается довольно просто и тут не нужно быть гением. Пожертвуем немного точностью и безопасностью. Класс буду приводить полностью. Если хотите подумать жать на спойлер не обязательно.
Вариант номер раз
package com.calculator;

import javax.script.ScriptEngineManager;

import java.io.PrintStream;

public class Calculator1 {

    /**
     * Самый простой, но менее точный результат. Что с безопасностью?
     * @param expression выражение для расчета
     */
    private static void calc(String expression) {
        try {
            System.out.println(new ScriptEngineManager().getEngineByName("JavaScript").eval(expression));
        } catch (Exception ex) {
            try (PrintStream stream = (System.out.append("Nan"))) {}
        }
    }

    public static void main(String[] args) {
        calc("+5 + -12");
        calc("+5 * -12");
        calc("+5 - -12");
        calc("+5 / -12");
    }
}

В 2008 году такой трюк бы не прошел, поэтому человек X решил эту задачу по своему. Примечание: код все равно адаптирован под java 7, уж простите.

Вариант номер два
package com.calculator;

import java.io.PrintStream;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Calculator2 {

    /**
     * Вариант, который будет предложен в том или ином виде большинством
     * @param expression выражение для расчета
     * @param args хитрость, до которой стоит догадаться
     */
    private static void calc(String expression, Object ... args) {
        try {
            // 1. Отображаем результат
            System.out.println(
            // 2. Ищем метод по коду операции
            BigDecimal.class.getMethod(
            Arrays.asList("multiply", "add", "subtract", "divide").get(
            ((args = new Matcher[] {
            Pattern.compile(
            // 3. Регулярка для анализа выражения. Отмечу, что регулярка то и не очень важна, ее можно допилить так как хотите.
            "[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?\\s*([+-\\\\*/])\\s*[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$")
            .matcher(expression)})) != null &&
            ((Matcher) args[0]).find() ?
            // 4. Коды символов основных операций 42: '*', 43: '+', 45: '-', 47: '/' - простая формула дает индексы 0, 1, 2, 3
            ((int) ((Matcher) args[0]).group(1).charAt(0) - 41) / 2 : -1),
            // 5. Вычисляем результат
            BigDecimal.class, MathContext.class).invoke(
            // 6. Первый аргумент пошел
            new BigDecimal(((args = new Matcher[] {
            Pattern.compile("([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?)").matcher(expression)})) != null &&
            ((Matcher) args[0]).find() ? ((Matcher) args[0]).group(0) : ""),
            // 7. Второй аргумент пошел
            new BigDecimal(((args = new Matcher[] {
            Pattern.compile("[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?\\s*[+-\\\\*/]\\s*([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?)$")
            .matcher(expression)})) != null && ((Matcher) args[0]) .find() ? ((Matcher) args[0]).group(1) : ""), new MathContext(10, RoundingMode.HALF_EVEN)));
        } catch (Exception ex) {
            /** Хитрый трюк сказать пользователю что выражение фиговое */
            try (PrintStream stream = (System.out.append("Nan"))) {}
        }
    }

    public static void main(String[] args) {
        calc("+5 + -12");
        calc("+5 * -12");
        calc("+5 - -12");
        calc("+5 / -12");
    }
}


Как в известной песни: Ну что сказать, ну что сказать, устроена так java, желают знать, желают знать, желают знать, что будет…

Но признаемся себе, этот код слишком громоздкий и имеет повторения. А что если усилить ограничение и потребовать не использовать тернарный оператор вовсе? Не сразу, но решение все же нашлось.

Вариант номер три
package com.calculator;

import java.io.PrintStream;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Calculator3 {

    /**
     * Вариант, без тернарного оператора, здесь нужно немного подумать.
     * @param expression выражение для расчета
     * @param args хитрость, до которой стоит догадаться
     */
    private static void calc(String expression, Object ... args) {
        try {
            // 1. Отображаем результат
            System.out.println(
            // 2. Ищем метод по коду операции
            BigDecimal.class.getMethod(
            Arrays.asList("multiply", "add", "subtract", "divide").get(
            // 3. Запоминаем все требуемые значения в args и достаем код операции
            (Integer) (args = new Object[] {args = new Object[] {
            Pattern.compile("([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?)\\s*([+-\\\\*/])\\s*([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?)$").
            matcher(expression)}, args[0], ((Matcher) args[0]).find(), ((Matcher) args[0]).group(1), ((int) ((Matcher) args[0]).group(2).charAt(0) -41) / 2,
            ((Matcher) args[0]).group(3)})[4]),
            // 4. Вычисляем результат
            BigDecimal.class, MathContext.class).invoke(
            // 5. Первый аргумент пошел
            new BigDecimal(args[3].toString()),
            // 6. Второй аргумент пошел
            new BigDecimal(args[5].toString()), new MathContext(10, RoundingMode.HALF_EVEN)));
        } catch (Exception ex) {
            /** Хитрый трюк сказать пользователю что выражение фиговое */
            try (PrintStream stream = (System.out.append("Nan"))) {}
        }
    }

    public static void main(String[] args) {
        calc("+5 + -12");
        calc("+5 * -12");
        calc("+5 - -12");
        calc("+5 / -12");
    }
}


Так гораздо короче и без повторений, но мозг вот вот взорвется. Я думаю найдется еще пару решений.

Интересно, а как думают парни, излагающие свои мысли на scala или kotlin или c# или …, если указанные ограничения пусть и с допущениями — подходят?

Заключение


Спасибо дорогой читатель, за твое внимание и терпение. Как и обещал даю свой ответ на поставленный вопрос: 'Необычный код — искусство или порок?'. Я бы сказал так: 'Глазами экспериментатора — исскуство, глазами продакшена — порок'. Но как бы такой код не называли, помни, ты можешь попробовать. Отдельно хочу извиниться перед жителями habrahabr за выбранный стиль изложения, если что не так. Это экспериментальный с моей стороны подход, спасибо за понимание.

Комментарии (9)

  • 30 января 2017 в 21:04

    +3

    Извините, а причем тут C#?(в хабах)

    • 30 января 2017 в 21:57

      –2

      Я рассуждал так, что перечисленные языки очень похожи, и хотелось увидеть схожие решение на с#

    • 30 января 2017 в 22:36

      +1

      Исправился
  • 30 января 2017 в 21:39 (комментарий был изменён)

    0

    JS можно?


    вот без eval:


    ((s)=>({'+':(a,b)=>+a+(+b),'-':(a,b)=>a-b,'*':(a,b)=>a*b,'/':(a,b)=>a/b})[s[1]](s[0],s[2]))('9/3')

    уупс, только с одним разрядом и с положительными работает, сейчас поправлю

    • 30 января 2017 в 21:55

      0

      Все равно зачет!!!

    • 30 января 2017 в 21:55 (комментарий был изменён)

      0

      Так лучше:


      ((s)=>((_,a,op,b)=>({'+':(a,b)=>+a+(+b),'-':(a,b)=>a-b,'*':(a,b)=>a*b,'/':(a,b)=>a/b})[op](a,b))(...s.match(/(-?\d+)\s*([+\-*/])\s*(-?\d+)/)))('-10+12')
  • 30 января 2017 в 22:03

    +1

    Python 2:


    while True:
        print input('> ')

    Python 3:


    while True:
        print(eval(input('> ')))

    Python (любой):


    import sys
    
    while True:
        if sys.version_info >= (3, 0):
            print(eval(input('> ')))
        else:
            print(input('> '))

    trollface.jpg

    • 30 января 2017 в 22:13 (комментарий был изменён)

      0

      Ruby:


      puts eval gets

      С циклом:


      loop do puts eval gets end
  • 30 января 2017 в 22:16 (комментарий был изменён)

    0

    «Офигенный парсер» вполне уложился бы в полтора часа. Тут ведь ограничение на два числа, никаких вложенных выражений, никаких скобок, никаких приоритетов операций, весь разбор — тупой конечный автомат, десяток состояний от силы.

© Habrahabr.ru