Вводная


Как это часто бывает, когда Вы ищете работу, Вы проходите одно собеседование за другим. Где-то Вас выбирают, где-то Вы. И наверное, в жизни каждого из нас бывали интересные собеседования, о которых можно с удовольствием поведать публике. Я хочу рассказать об одной такой истории, где есть место эмоциям, панике, потоку мышления и вдохновению. Речь в статье пойдет о внутренних переживаниях соискателя, о его противостоянии с интервьюером, интересный и мозговзрывательный код на 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. Регулярка для анализа выражения. Отмечу, что регулярка то и <b>не очень важна</b>, ее можно допилить так как хотите.
            "[+-]?(?:\\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 за выбранный стиль изложения, если что не так. Это экспериментальный с моей стороны подход, спасибо за понимание.
Поделиться с друзьями
-->

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


  1. ColdPhoenix
    30.01.2017 21:04
    +7

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


    1. reforms
      30.01.2017 21:57
      -9

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


      1. Alex_ME
        31.01.2017 00:37
        +6

        Что же, вот вам код на C#. Если использование LINQ покажется вам читерством, то легко заметить, что его можно переписать без LINQ, правда куски буду много раз повторятся (убирание пробелов, регулярка итп)

         static double Calc(string expr)
                {
                    return (from e in new[] {expr}
                        let e_clear = e.Replace(" ", "")
                        let match = Regex.Match(e_clear.Substring(1), @"[+\-*/]")
                        let operation = match.Value
                        let operation_index = match.Index + 1
                        let first = double.Parse(e_clear.Substring(0, operation_index))
                        let second = double.Parse(e_clear.Substring(operation_index + 1))
                        let result =
                            operation == "+"
                                ? first + second
                                : (operation == "-"
                                    ? first - second
                                    : (operation == "*" ? first*second : (operation == "/" ? first/second : double.NaN)))
                        select result).FirstOrDefault();
                }
        


        1. planarik
          31.01.2017 09:08

          var dt=new DataTable();
          var row=dt.Select("1+2");
          var result=row[0][0];


        1. reforms
          31.01.2017 13:38

          спасибо


    1. reforms
      30.01.2017 22:36
      +1

      Исправился


  1. f0rk
    30.01.2017 21:39

    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')

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


    1. reforms
      30.01.2017 21:55

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


    1. f0rk
      30.01.2017 21:55
      +1

      Так лучше:


      ((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')


  1. VovanZ
    30.01.2017 22:03

    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


    1. handicraftsman
      30.01.2017 22:13
      +2

      Ruby:


      puts eval gets

      С циклом:


      loop do puts eval gets end


    1. synedra
      31.01.2017 08:05
      +2

      На питоне можно и интереснее сделать.
      Основная идея похожа на то, что приводили выше на JS, но за счёт нетривиального использования генератора списка позволяет избежать импорта вообще.

      def calc(line):
          print([item[1](int(line.split(item[0])[0]), int(line.split(item[0])[1])) for item in {'*': lambda x,y: x*y, '+': lambda x,y: int(x)+int(y), '-': lambda x,y: x-y, '/': lambda x,y: x/y}.items() if item[0] in line][0])
      

      Собственно, идея понятна. Считаем список, в котором будут результаты умножения чисел (если есть `*`), сложения (если есть `+`) и так далее. Поскольку по условиям задачи команда только одна, достаточно взять из этого списка первый элемент — он всё равно окажется единственным.
      До полноценного однострочника не хватает только убрать объявление функции. Код и без того не особо читаемый, так что приведу только идею. (голосом парня из Креосана) Нужно больше генераторов!
      [all_that_stuff_above for line in [input('> ')]][0]
      

      Очевидно, `all_that_stuff_above` — это тело функции сверху. Пока только не могу сообразить, как ввернуть сюда бесконечный цикл.


      1. synedra
        31.01.2017 08:15

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


      1. ApeCoder
        31.01.2017 13:11
        +1

        есть модуль оператор чтобы не оборачивать все лямбдами


        1. synedra
          31.01.2017 15:03

          Симпатично, не знал про такой модуль. С ним, правда, немного сложней: я для себя переформулировал задачу как «написать однострочник, который можно вставить в интерактивную консоль и получить результат», а как засунуть в ту же строчку импорт (не считая читерства с ';'), не придумал. По той же причине решение без регэкспов.


  1. sshikov
    30.01.2017 22:16
    +3

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


  1. yizraor
    31.01.2017 00:05

    Побаловался со стримами, текст проги из-за объёма спрятал под спойлером.
    И, это, прошу больно не бить — пока ещё только изучаю Яву :)

    Текст программы
    import java.io.PrintStream;
    import java.util.function.BinaryOperator;
    import java.util.stream.*;
    
    public class OneLineCalc
    {
      private static void calc(String s)
      {
        try
        {
          System.out.println
          (
            Stream.of(BinaryOperator.maxBy(Double::compare), (a,b) -> a+b, (a,b) -> a-b, (a,b) -> a*b, (a,b) -> a/b)
              .collect(Collectors.toList())
              .get
              (
                IntStream.iterate(1, i -> i + 1)
                  .limit(s.replaceAll("\\s", "").length() - 1)
                  .map(i -> 1 + "+-*/".indexOf(s.replaceAll("\\s", "").charAt(i)))
                  .filter(i -> i > 0)
                  .limit(1)
                  .sum()
              )
              .apply
              (
                IntStream.iterate(1, i -> i + 1)
                  .limit(s.replaceAll("\\s", "").length() - 1)
                  .filter(i -> "+-*/".indexOf(s.replaceAll("\\s", "").charAt(i)) >= 0)
                  .limit(1)
                  .mapToDouble(i -> Double.parseDouble(s.replaceAll("\\s", "").substring(0, i)))
                  .sum(),
                IntStream.iterate(1, i -> i + 1)
                  .limit(s.replaceAll("\\s", "").length() - 1)
                  .filter(i -> "+-*/".indexOf(s.replaceAll("\\s", "").charAt(i)) >= 0)
                  .limit(1)
                  .mapToDouble(i -> Double.parseDouble(s.replaceAll("\\s", "").substring(i+1)))
                  .sum()
              )
          );
        }
        catch (Exception e)
        {
          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");
      }
    }
    


    1. lany
      31.01.2017 11:11
      +2

      Вы имели в виду как-то так?


      import java.util.*;
      import java.util.function.DoubleBinaryOperator;
      import java.util.stream.*;
      
      public class OneLineCalc {
        private static final String OP_NAMES = "+-*/";
        private static final List<DoubleBinaryOperator> OPS =
                Arrays.asList((a, b) -> a+b, (a, b) -> a-b, (a, b) -> a*b, (a, b) -> a/b);
      
        private static String normalize(String s) {
          return s.replaceAll("\\s", "");
        }
      
        private static void calc(String s) {
          System.out.println(doCalc(normalize(s)));
        }
      
        private static double doCalc(String s) {
          try {
            int opPos = IntStream.range(1, s.length())
                                 .filter(idx -> OP_NAMES.indexOf(s.charAt(idx)) != -1)
                                 .findFirst().getAsInt();
            return OPS.get(OP_NAMES.indexOf(s.charAt(opPos))).applyAsDouble(
                      Double.parseDouble(s.substring(0, opPos)),
                      Double.parseDouble(s.substring(opPos+1)));
          }
          catch (NoSuchElementException | NumberFormatException e) {
            return Double.NaN;
          }
        }
      
        public static void main(String[] args) {
          calc("+5 + -12");
          calc("+5 * -12");
          calc("+5 - -12");
          calc("+5 / -12");
        }
      }


      1. yizraor
        31.01.2017 11:37
        +1

        нет, не совсем :))
        я имел в виду те условия, которые описываются автором:

        Необходимо написать калькулятор для простого выражения, который бы содержал ровно 1 строчку кода и умел складывать, вычитать, умножать и делить 2 числа. 1 строчка — означает ровно 1 точку с запятой

        и кстати, я упростил свой вариант функции calc():
          IntStream.iterate(1, i -> i + 1)
            .limit(expr.replaceAll("\\s", "").length() - 1)
            .filter(i -> "+-*/".indexOf(expr.replaceAll("\\s", "").charAt(i)) >= 0)
            .limit(1)
            .mapToDouble
            (
              i -> Stream.of
                (
                  BinaryOperator.maxBy(Double::compare),
                  (a,b) -> a + b, (a,b) -> a - b, (a,b) -> a * b, (a,b) -> a / b
                )
                .collect(Collectors.toList())
                .get(1 + "+-*/".indexOf(expr.replaceAll("\\s", "").charAt(i)))
                .apply
                (
                  Double.parseDouble(expr.replaceAll("\\s", "").substring(0, i)),
                  Double.parseDouble(expr.replaceAll("\\s", "").substring(i + 1))
                )
            )
            .forEach(f -> System.out.println(f));
        
        


        1. lany
          31.01.2017 12:06
          +1

          Понял. С помощью Optional и flatMap легко наплодить вложенных скоупов в одном выражении, например, так:


          System.out.println(Optional.of(str)
              .map(s -> s.replaceAll("\\s", ""))
              .flatMap(s -> IntStream.range(1, s.length())
                  .filter(idx -> "+-*/".indexOf(s.charAt(idx)) != -1)
                  .boxed().findFirst().map(opPos -> Arrays.<DoubleBinaryOperator>asList(
                      (a, b) -> a+b, (a, b) -> a-b, (a, b) -> a*b, (a, b) -> a/b)
                          .get("+-*/".indexOf(s.charAt(opPos))).applyAsDouble(
                           Double.parseDouble(s.substring(0, opPos)), 
                           Double.parseDouble(s.substring(opPos+1)))))
                  .orElse(Double.NaN));

          Остаётся обработка исключения при parseDouble. Мне не нравится оригинальный вариант, это всё же не "одно выражение". Можно использовать CompletableFuture для обработки исключений:


          ToDoubleFunction<String> parser =
              s -> CompletableFuture.completedFuture(s)
                .thenApply(Double::parseDouble)
                .handle((d, t) -> t != null ? Double.NaN : d).join();

          При этом парсер можно опять же закинуть в скоуп через Optional, чтобы избежать дублирования кода. Окончательно:


          System.out.println(Optional.<ToDoubleFunction<String>>of(s -> 
              CompletableFuture.completedFuture(s).thenApply(Double::parseDouble)
                               .handle((d, t) -> t != null ? Double.NaN : d).join())
              .flatMap(parser -> Optional.of(str)
                  .map(s -> s.replaceAll("\\s", ""))
                  .flatMap(s -> IntStream.range(1, s.length())
                      .filter(idx -> "+-*/".indexOf(s.charAt(idx)) != -1)
                      .boxed().findFirst().map(opPos -> Arrays.<DoubleBinaryOperator>asList(
                          (a, b) -> a+b, (a, b) -> a-b, (a, b) -> a*b, (a, b) -> a/b)
                              .get("+-*/".indexOf(s.charAt(opPos))).applyAsDouble(
                                  parser.applyAsDouble(s.substring(0, opPos)),
                                  parser.applyAsDouble(s.substring(opPos+1))))))
              .orElse(Double.NaN));

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


          1. yizraor
            31.01.2017 12:10

            класс!
            большое спасибо за комментарий и куски кода :)


            1. yizraor
              31.01.2017 13:12

              допилил свой вариант, чтобы [почти] без дублирования кода и при частичном усвоении подсмотренного :)
              теперь уже покрасивше…

              Получившийся код
              package onelinecalc;
              
              import java.io.PrintStream;
              import java.util.Arrays;
              import java.util.function.BinaryOperator;
              import java.util.stream.*;
              
              public class OneLineCalc
              {
                private static void calc(String s)
                {
                  try
                  {
                    Arrays.asList(s.replaceAll("\\s", "")).forEach
                    (
                      expr -> System.out.println
                      (
                        IntStream.range(1, expr.length())
                          .filter(i -> "+-*/".indexOf(expr.charAt(i)) >= 0)
                          .mapToDouble
                          (
                            i -> Arrays.<BinaryOperator<Double>>asList
                              ( (a,b) -> a + b, (a,b) -> a - b, (a,b) -> a * b, (a,b) -> a / b )
                              .get("+-*/".indexOf(expr.charAt(i)))
                              .apply
                              (
                                Double.parseDouble(expr.substring(0, i)),
                                Double.parseDouble(expr.substring(i + 1))
                              )
                          )
                          .findFirst().orElse(Double.NaN)
                      )
                    );
                  }
                  catch (Exception e)
                  {
                    try ( PrintStream stream = new PrintStream(System.out).append("NAN") ) { }
                  }
                }
                
                public static void main(String[] args)
                {
                  calc("+5 + -12");
                  calc("+5 - -12");
                  calc("+5 * -12");
                  calc("+5 / -12");
                }
              }
              


      1. yizraor
        31.01.2017 11:42

        а вобще-то да, суть кода та же самая, только я не сразу сообразил, как свой вариант упростить.
        по сути у Вас то же самое, но «лишняя точка с запятой» :)

        и за .findFirst() спасибо, я не был уверен, что он отработает именно так, как мне надо — чтобы без .limit().
        потестю вечерком этот момент с iterate() + findFirst().
        и про range() я что-то забыл, вот оно что…


  1. zip_zero
    31.01.2017 00:40
    -3

    Я всё жду, что здесь повторят ситуацию на SO, когда набежали уважаемые господа и начали предлагать паттерны, а потом рассказывать про лексеры, парсеры и абстрактные синтаксические деревья :)


    1. grossws
      31.01.2017 11:25
      +1

      Вариант с простым лексером (на два типа токенов: число и знак операции) и простым парсером без приоритетов операций без опыта такого рода вещей (в смысле написания парсеров и подобной алгоритмики) из общих соображений пишется за час-полтора (с тестами и перекурами). Вчера проверял ,)


      1. zip_zero
        31.01.2017 12:38
        -1

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

        Ради спортивного интереса: можно пожалуйста в студию ваш код?


        1. grossws
          31.01.2017 12:51

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


          Вечером постараюсь не забыть закинуть в gist.


          1. grossws
            31.01.2017 22:34

            Мой вариант: https://gist.github.com/grossws/b37cef5f24489dd63ac58cce718c79c1.


            Все бинарные операции правоассоциативны, поэтому в некоторых тестах есть неочевидная хрень: -0.2 + -0.3 * 5.00 == -1.7, но 5. * -0.3 - 0.2 == -2.5.


            Общее время написания около 1.5 часов, если правильно помню. Из которых минут 15 ушло на поиск неприятного бага из-за неявного lookahead'а в токенайзере, который не проявлялся, если операции отбиты пробелами.


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


            1. grossws
              31.01.2017 22:36

              Дополнение: в контексте такой задачи на собеседовании сначала бы написал тривиальный вариант на 1 регулярке и 3 if'а, а потом бы развлекался с лексером и парсером. Или взял бы antlr, например ,)


              1. sshikov
                01.02.2017 09:54

                Решение на antlr меня бы уже заставило призадуматься — потому что перебор. Явный.


                1. grossws
                  01.02.2017 12:36

                  Для этой задачи почти всё перебор кроме простых автоматов (ручных или внутри регулярки).


                  А если развлекаться или работа предполагает написание парсеров, то в качестве дополнения к простому решению можно и с antlr/javacc поиграться. Или реализовать более сложную грамматику, как вариант.


                  1. sshikov
                    01.02.2017 12:53

                    Насколько я помню, на основе antlr за полтора часа я делал парсер чего-то типа JSON. Так что для этой задачи он как из пушки по воробьям.


        1. zagayevskiy
          31.01.2017 13:58

          Тут можно найти мой код на эту тему. Могу сказать, что написание полностью работающего калькулятора с приоритетами, скобками и кучей операций заняло у меня где-то в районе 8 часов (поездка на Сапсане Мск-СПб и обратно). За полтора часа можно написать упрощённый вариант, который будет читаемее предложенных и в котором не будет багов.


        1. sshikov
          31.01.2017 21:26

          Я вам скажу, как бы я оценивал с точки зрения человека, который тестирует:

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

          Человек, который претендует на что-то большее, чем быдлокодер (а иначе, нафига вообще давать такую задачу?), должен быть способен рассказать про рекурсивный спуск. А если сможет еще про что-то, типа parsec — будет идеально. Оба — прямо скажем тривиальны.

          Ну и все это применить к конкретной задаче, и показать (с оценками в часах), что ее можно сделать на регулярках, или на стримах, и почему написание LL(1) тут будет уже перебором.

          И можно код вообще не писать.


          1. bamovetz
            31.01.2017 22:55
            +1

            А вы не задумывались о том что разработчик не обязан держать в голове все возможные алгоритмы и области применения. Если мне потребуется парсить выражение — то я пороюсь в инете и выберу лучший для решения способ и потом его реализую(или использую готовый) в контексте задачи.
            А ваша позиция пригодна для тестирования студента на знание алгоритмов и выявление потенциала его как программиста.
            С опытом приходит понимание что все это детские игры. Я с подобных собеседований ухожу сразу. Ибо работать там скорее всего будет не комфортно.
            Главное для реально хорошего программиста — это умение находить решение оптимальное для задачи, а не заучивать регулярные выражения и вариации парсеров. Поиск и обработка информации — наше все.


            1. sshikov
              01.02.2017 09:48

              Заучивать регулярные выражения? Это вы где у меня такую ерунду вычитали?


              1. bamovetz
                01.02.2017 11:11

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


                1. sshikov
                  01.02.2017 11:38

                  Неточно — это мягко говоря. Я нигде не предлагал ничего заучивать, и ничего такого тестировать.

                  Но человек, который претендует быть программистом, просто обязан понимать, что регулярные выражения, скажем, либо совсем неприменимы для разбора языков с вложенными структурами, либо сильно ограничены для таких задач. Что внутри у них, упрощенно — конечный автомат, а стека наоборот внутри нет. Он просто обязан знать, что такое грамматики, и какими хотя бы примерно методами с ними работают.

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


                  1. bamovetz
                    01.02.2017 23:05

                    И вот опять вы про студента-отличника или узкого специалиста.
                    Я например несмотря на то что учился в институте на профессию инженер-программист — грамматики не изучал. В работе мне их знание никогда не требовалось. Причем я не веб-разработчик. А программист на C++ в энтерпрайзе.
                    Во всех местах где я работал была совершенно новая для меня предметная область. Например в текущем месте работа криптография и смарт-карты. Это не помешало мне в ней разобраться и работать ведущим разработчиком.
                    Непосредственные заученные знания это пыль на ветру. Развеивается сразу после заучивания. Важно знать основы. Методологию. Изучать их постоянно. И главное — то чему меня в моем универе научили — это уметь получать новые знания когда требуется.
                    По вашему описанию постановки задачи — соискателю должен быть дан еще и доступ к интернету и время для поиска.


                    1. sshikov
                      02.02.2017 10:07

                      Я пытаюсь оставаться в контексте этого поста. Если человеку дали такую задачу — я считаю, что был повод дать именно такую. При чем тут ваша криптография, которая, разумеется, имеет место и право на жизнь?

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


                      1. bamovetz
                        02.02.2017 10:44

                        Про криптографию не понял претензии. Я привел пример меня любимого. То что я в криптографии был полным нубом при устройстве и мне это не помешало в короткие сроки изучить все что надо.
                        По поводу языков и грамматик — это типичнейшее заблуждение выпусников постсоветских вузов. Это совсем не базовые структуры и незнание их ничего не меняет ибо действительно разбираться в этом требуется только в узкой области разработки.
                        Что не отменяет полезности общих знаний по данной теме, как и вообще постоянной учебы.


                        1. sshikov
                          02.02.2017 11:05

                          Да нету никаких претензий! Я совсем про другое. Если человеку на интервью дают подобную описанной задачу — от него ждут каких-то познаний не в криптографии, а все-таки в области парсеров, регулярных выражений, конечных автоматов, и всего такого. И это нормально.

                          Что же до грамматик — то что вы знаете о том, какой ВУЗ я закончил? И да, их базовость состоит не в том, что они везде нужны — а в том, что они, по сути, достаточно тривиальны, и никакого ricket science не содержат. И если человек их не знает вовсе — это если не повод его браковать, то повод задуматься о его квалификации.


                          1. bamovetz
                            02.02.2017 14:33

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

                            По поводу ВУЗа — я сужу по тому что грамматики и языки программистам стали преподавать после развала союза. До этого в основном налегали на базовую математическую основу. И единственный вариант что вы их изучали в то время — тот что вы учились на другой специальности — например «Прикладная математика».
                            По поводу нужности грамматик — я с вами расхожусь кардинально. Я считаю что они нужны только для общего образования и к квалификации не имеют никакого отношения. И проблема не в тривиальности а в том что все знать невозможно. И за 10 лет работы в разработке вы можете столкнутся с тем что они вам ни разу не понадобятся.


                            1. sshikov
                              02.02.2017 14:57

                              Я исхожу из того, чтобы не додумывать за других. Если такая задача была — значит ее зачем-то дали.

                              Область эта не узкая. У меня 90% задач состоят и состояли в том, чтобы парсить или генерировать какие-то потоки данных, текстовые или бинарные, от других приложений, к которым нет API. Считать конечные автоматы узкой областью (или того пуще — регулярные выражения) — это странно.

                              Вы, собственно, пишете на каком-то языке — и вам не любопытно, как устроен компилятор? Не, вы не обязаны — но неужели любопытства никогда не проявлялось?

                              И вы кстати ошибаетесь насчет ВУЗ-а. Я вообще по специальности инженер конструктор. 1981 год. Компиляторы у нас преподавали уже тогда. Не нам. Нам преподавали сопромат.


                              1. bamovetz
                                02.02.2017 17:30

                                Учитывая фразу «удиви меня» — ее дали не из за требований вакансии а по причине выпендрежности программиста-собеседователя.
                                Для уточнение расскажите о своей области. Пока у меня складывается мнение что именно узкая или вообще веб-разработка.
                                По поводу любопытства вы явно не читаете что я написал. Я же сказал что для общего образования и развития желательно изучать. Но на собеседованиях спрашивать только в исключительных случаях, мной описанных.
                                По поводу ВУЗа — вы опять невнимательно прочитали. Я именно что прав и вы это подтвердили. Так как получили образование инженера конструктора. Я учился по программе инженера программиста в университете — там грамматик и языков не было. А то что называется компиляторы — тоже было но без грамматик и конечных автоматов и тем более без написания компиляторов. Только базовые основы. Из математики только машина тьюринга.


                    1. Maccimo
                      03.02.2017 11:36
                      +1

                      Я например несмотря на то что учился в институте на профессию инженер-программист — грамматики не изучал. В работе мне их знание никогда не требовалось.

                      Здраво оценивать область применимости неких знаний может только человек, этими знаниями владеющий. Хотя бы на базовом уровне.


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


                      1. bamovetz
                        03.02.2017 19:14

                        В принципе верно. Но мы говорим про то базовые это знания или нет для программиста.
                        Я считаю что нет. При этом для общего образования надо знать на базовом уровне.
                        Но есть один момент грамматики это вещь ИМХО довольно узкая и нужна для разработчиков языков например и еще для нескольких таких же узких областей.
                        И в дополнение — если вы умеете учиться (а это основной навык хорошего программиста) то всегда быстро научитесь как что-либо применять. Да и не забывайте мы живем в мире интернета — а значит там можно выяснить что в решении стоящей перед вами задачи подойдут грамматики, после чего их можно будет изучить.


  1. zip_zero
    31.01.2017 01:34
    +7

    Если серьёзно, то первый пример самый крутой (из-за простоты и ясности), а что касается последних двух — я даже не подозревал, что где-то есть люди, которым хватит силы воли написать такое.

    Что касается искусства: второй и третий пример хорошо воспринимают строку

    calc("ыы5*2");
    

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


  1. zenkz
    31.01.2017 01:58
    +3

    Чтоб вам всю жизнь такой код поддерживать! За такой код в продакшине нужно убивать.

    Вот интересно, а что ожидал увидеть интервьювер, давая такое бесполезное задание? И совпало ли ожидание с реальностью?

    Если бы я проводил интервью с таким заданием, то я бы посмотрел на лаконичность и понятность кода, наличие комментариев / документации к методам, именование переменных и соблюдение стандартов оформления, отсутствие повторяющегося кода, лёгкость модификации.


    1. avost
      31.01.2017 03:06
      -5

      Чтоб вам всю жизнь такой код поддерживать! За такой код в продакшине нужно убивать

      А где здесь хоть пол-слова о продакшене? Вы техническое задание (статью) не читали вовсе, что ли? А свои технические задания так же читаете? ;)


      интервьювер, давая такое бесполезное задание?

      Для кого бесполезное? Для компании? В смысле, код соискателя бесплатно не потыришь и не вставишь в продакшн (а, я теперь понял почему вы про продакшен говорили)? Для этой цели абсолютно бесполезен. В точку!


      Если бы я проводил интервью с таким заданием, то я бы посмотрел на лаконичность и понятность кода, наличие комментариев / документации к методам, именование переменных и соблюдение стандартов оформления, отсутствие повторяющегося кода, лёгкость модификации.

      После получения такого решения на эту вашу муть можно не смотреть. Вы просто ищите работника в другом классе. В ортогональном. Это не хорошо и не плохо, ваш класс задач тоже имеет право на существование. Тут странно только одно — чего это вас так взбесил сам факт существования другой вселенной. По-моему, получив такое решение, в вашем случае стоило бы крепко пожать руку соискателю и сказать, — дорогой друг, вам, скорее всего, у нас будет бесконечно скучно и нас будут терзать угрызения совести от того, что вы из-за нас терпите такую муку, давайте не будем причинять друг-другу боль :).
      ЗЫ, думаю, автору отказали, но по другой причине…


      1. aamonster
        31.01.2017 07:52
        +2

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


        Хотя на будущее код можно сохранить и давать соискателям задание отрефакторить его.


        1. avost
          31.01.2017 19:15

          Можно и так, если совсем делать нечего и есть непреодолимое желание измучить соискателя вусмерть. Только смысл не совсем понятен. Вы полагаете, человек, заморочившийся на написание такого кода не сможет сделать это: "лаконичность и понятность кода, наличие комментариев / документации к методам, именование переменных и соблюдение стандартов оформления, отсутствие повторяющегося кода, лёгкость модификации"? Серьёзно? Ну, это, конечно ваше право, но в тех конторах, куда я ходил до сих пор на собеседования и собеседователи попадались более профессиональные и относились они к претендентам более уважительно. Наверное, везло. Пусть и дальше везёт.


          код можно сохранить и давать соискателям задание отрефакторить его

          На этом месте я сразу вспомнил местную хабрадискуссию нескольколетней давности, когда пэхпешники всерьёз обсуждали вопрос о том, что простейший тернарный оператор слишком сложен для понимания и его следует "отрефакторить" в if-else конструкцию длиной от 5 до 8 строчек. :)


      1. zenkz
        31.01.2017 19:14

        Не нужно путать олимпиады по программированию / Code Retreat / хакатоны с трудоустройством.

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

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

        Сначала я подумал, что подобные решения могут быть интересны чтобы выъявить кандидатов для задач в научной и research сферах. Но всё-же и там важнее не написать в одну строчку, а написать так, чтобы потом было легко модифицировать, отлаживать и поддерживать.


        1. avost
          31.01.2017 20:35
          +1

          Почему олимпиадные? Интервьюер получил ровно то, что получил. Ни каплей больше, ни каплей меньше.
          Соискатель же строго в рамках сформулированного задания показал а) умение программировать б) умение мыслить в) умение мыслить нестандартно г) умение рассчитать свои силы, придумать решение, спроектировать и выполнить нетривиальным образом задание практически в строго отведённый срок и тд и тп.
          Не знаю как вам, но это прекрасные навыки к отличного инженера. В большинстве случаев вообще достаточно пункта "а", а уж пункт "г" лично меня повергает прямо таки в священный трепет! :)


          Ваши требования — умение оформлять код (нажать две плюс две кнопки? офигенное умение!), умение писать комментарии, умение именовать переменные. Это требования на какую, простите, должность? На должность стажёра, пытающегося устроиться за еду для строчки в резюме?


          1. fogone
            01.02.2017 11:04

            Справедливости ради нужно заметить, что «умение оформлять код, умение писать комментарии, умение именовать переменные» — это те умения, которым учишься и совершенствуешься в них беспрерывно — будь ты хоть «стажер за еду» или синьёр за многок/мес. Особенно это касается последнего пункта.


            1. avost
              01.02.2017 15:28
              -1

              Да, ладно!
              "умение оформлять код" вообще никому не нужно — это одна кнопка в IDE.
              "умение именовать переменные" чуть более, чем ненужно. Код метода должен умещаться на экран, все переменные видны и что-то не понять сложно. Намного важнее умение именовать методы. И не писать их длиннее, чем на один экран :)
              "умение писать комментарии"… даже не знаю что сказать… оно проистекает из неумения писать ясный код, правда же? :)
              Ценно неупомянутое умение писать джавадоки к апи. Но таким способом его не проверишь. И это, скорее, не к очень сильно рядовому разработчику (а требования у zenkz относятся не более, чем к таковому), а к архитектору.


              1. fogone
                01.02.2017 16:54
                +1

                Похоже, мы тут в терминологии заблудились. Потому что «умение оформлять код» это не только его форматирование, но и например где нужно выделить в отдельные переменные для понятности, где-то наоборот для наглядности сделать цепочку, грамотно упорядочить методы в класс и тому подобное. «умение именовать переменные» — именование именно переменных нужно реже, чем методов, их параметров или классов (хотя я и имел ввиду именование в целом), но всё же имеет не меньшую важность, да и сложность примерно ту же. Короткие методы, к слову, от этого не спасают. «умение писать комментарии» — это в первую очередь простое и понятное написание javadoc-ов. Так уж повелось, что доки пишутся в комментариях, потому это фактически является написанием комментариев, а от того часто так и называются.


                1. avost
                  01.02.2017 19:50
                  -1

                  Как у вас всё круто, оформление кода — это не оформление кода, а другое, умение именовать переменные — это не умение именовать переменные, а другое, и умение писать комментарии — это не умение писание комментарии, а другое. Одни пишу, два — в уме! Чтобы так сформулировать, надо особыми талантами обладать. Вы ТЗ, случайно не пишите, :)


                  А теперь смотрим мой комментарий — ваше "умение оформлять код" — это "моё" (и фактическое) умение писать код (умение программировать). Ваше "умение именовать переменные" — это "моё" (и фактическое) умение писать код (умение программировать). Ваше "умение писать комментарии" — это "моё" умение писать джавадоки для апи. Для API. Для рутинного кода достаточно имени класса и метода.


        1. grossws
          31.01.2017 22:40

          Но всё-же и там важнее не написать в одну строчку, а написать так, чтобы потом было легко модифицировать, отлаживать и поддерживать.

          Тоже далеко не всегда, если говорить про R&D. Часто необходимо написать сначала quick-and-dirty код, который никогда не будет поддерживаться для проверки гипотезы. Но понятность кода и легкость отладки остаются важными факторами.


    1. Jef239
      31.01.2017 09:28

      «history mode»Ну я в свое время выпендрился тем, что не хранил исходный текст выражения, а для печати делал рекомпиляцию из нитевидного кода. Если на УКНЦ работали, то может и видели эти электронные таблицы. Памяти было мало, а скомпилированное представление короче...</history mode>

      Выпендриваться надо на примере 1 / 3 * 15. В смысле чтобы получить там строго 5, а не 4.9999999999999. Можно результат округлять, можно аналитически решать… Вариантов много. Главное — понять, что пользователь от калькулятора ждет именно 5.


  1. datacompboy
    31.01.2017 02:05

    Взяли?


    1. Find_the_truth
      31.01.2017 13:07

      Вот. Мне тоже интересно. Не зря эти финты с кодом проводились-то?


    1. reforms
      31.01.2017 13:29
      +1

      Да


  1. vadim_ig
    31.01.2017 02:27
    +3

    Возможно, время позднее, и я не вижу очевидных вещей, но где же обещанное

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


    1. Stalker_RED
      31.01.2017 03:46

      Хоть это и не очень похоже на однострочник, но если отбросить импорты и тестовые вызовы calc(), то там действительно по одной точке с запятой, и всю эту лабуду можно записать в одну строку.


    1. arilou_camper
      31.01.2017 03:49

      Формально там одна строка. Формально)


  1. sens_boston
    31.01.2017 06:29

    На счет «парней с C#», то тут все действительно просто:

    C# код
    using System;
    using System.Data;
    
    namespace Calculator
    {
        class Program
        {
            static void Main(string[] args)
            {
                try { Console.WriteLine(new DataTable().Compute(string.Join(" ", args), null)); } 
                catch (Exception error) { Console.WriteLine(error.Message); }
            }
        }
    }
    


    1. kir_steyn
      01.02.2017 09:45

      чистый C-шный код

      В коде используется тип bool, хотя stdbool.h не подключен.
      Но вот
      char*&p
      

      это же ссылка на указатель, так что это C++, а не чистый Си.


      1. sens_boston
        01.02.2017 19:12

        Согласен, проглядел, код таки «плюсовый». Отредактировать пост, правда, не могу.
        P.S. Впрочем, код легко, при желании, превратить в «чисто сишный» — если кому-то на дешевых контроллерах потребуется string calculator :)


  1. Maccimo
    31.01.2017 08:45
    +2

    Почему-то не указан профильный хаб.


    /** Хитрый трюк сказать пользователю что выражение фиговое */
    try (PrintStream stream = (System.out.append("Nan"))) {}

    … и потом удивляться самому "почему у меня не печатает?".


    Самого главного так и не написали, в каких выражениях была сформулирована оценка этого произведения?


    1. reforms
      31.01.2017 13:31

      Готово


      1. reforms
        31.01.2017 13:36

        Почему-то не указан профильный хаб.
        Готово.

        Самого главного так и не написали, в каких выражениях была сформулирована оценка этого произведения?
        Интервьюер оказался человеком с юром, все прошло гладко. Человек X уже более 7 лет работает там и очень доволен.


  1. mkm565
    31.01.2017 09:55
    +1

    Это на С.


    int calc(char s)
    {
    return strchr(s,'+')?atoi(s)+atoi(strchr(s,'+')+1):strchr(s,'-')?atoi(s)-atoi(strchr(s,'-')+1):strchr(s,'
    ')?atoi(s)atoi(strchr(s,'')+1):strchr(s,'/')?atoi(s)/atoi(strchr(s,'/')+1):0;
    }


    Никаких проверок нет.


    Не работает, если первое число со знаком. Можно, конечно, извратиться и обработать и этот случай. Например, использовать strrchr — тогда второе число должно быть без знака.
    Знак второго числа проверять можно
    strchr(strchr(s,'+')+1,'-') ||? atoi(s)-atoi(strrchr(s,'-')+1): atoi(s)+atoi(strchr(s,'+')+1)


    Заменить atoi на atof для нецелых чисел.


    calc("ыы5*2"); <-- ыы5 заменит на 0


    1. AnROm
      31.01.2017 14:10

      Функция strchr() должна принимать указатель на строку, так что в calc нужно передавать char *s


  1. fr33zy
    31.01.2017 09:55
    +5

    Scala:


    import scala.util.matching.Regex
    import scala.util.matching.Regex.Groups
    
    def calc(s: String): Unit = {
      new Regex("""\s*([-+]?\d+)\s*([-+*/])\s*([-+]?\d+)\s*""")
        .findFirstMatchIn(s)
        .map {
          case Groups(a, op, b) => (a.toDouble, op, b.toDouble)
        }
        .map {
          case (a, "*", b) => a * b
          case (a, "/", b) => a / b
          case (a, "+", b) => a + b
          case (a, "-", b) => a - b
        }
        .foreach(println)
    }


  1. michael_vostrikov
    31.01.2017 10:19

    PHP, без eval и regexp.

    Скрытый текст
    (
      (($input = str_replace(' ', '', $input)) !== '')
      && (str_replace([0,1,2,3,4,5,6,7,8,9,' ','+','-','*','/','.'], '', $input) === '')
      && (
           ($pos = strpos($input, '+')) > 0 || ($pos = strpos($input, '+', $pos + 1)) > 0
        || ($pos = strpos($input, '-')) > 0 || ($pos = strpos($input, '-', $pos + 1)) > 0
        || ($pos = strpos($input, '*')) > 0
        || ($pos = strpos($input, '/')) > 0
      )
      && (($v1 = (float)substr($input, 0, $pos)) || true)
      && (($op =        substr($input, $pos, 1)) || true)
      && (($v2 = (float)substr($input, $pos + 1)) || true)
      && print(
           $op == '+' ? ($v1 + $v2) : (
             $op == '-' ? ($v1 - $v2) : (
               $op == '*' ? ($v1 * $v2) : (
                 $op == '/' ? ($v1 / $v2) : null
               )
             )
           )
         )
    ) || print('wrong expression format');
    


    1. michael_vostrikov
      02.02.2017 08:42
      -1

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


      function calc($input)
      {
          (preg_match_all('/^([+-]?\d+(?:\.\d+)?)\s*([\+\-\*\/])\s*([+-]?\d+(?:\.\d+)?)$/', $input, $a)
            && print(
              ($a[2][0] == '+'  ?  ($a[1][0] + $a[3][0])  :
                ($a[2][0] == '-'  ?  ($a[1][0] - $a[3][0])  :
                  ($a[2][0] == '*'  ?  ($a[1][0] * $a[3][0])  :
                    ($a[2][0] == '/'  ?  ($a[1][0] / $a[3][0])  :  null))))
            )
          ) || print('wrong expression format');
      }


  1. Ector
    31.01.2017 10:24

    Привет всем. Я разработчик С#, которому пришлось писать настоящий калькулятор. То есть, со скобками, приоритетами операций и даже с функциями. Правда, задача, чтобы это все работало из чистой строки, не стояла (у меня свой редактор формул), но это была работа точно не на полтора часа.
    Я так и не понял, в чем был смысл «теста». Проверить, умеет ли кандидат пользоваться регулярными выражениями? Так и без них там можно справиться. Я бы скорее проверил результат на устойчивость к ошибкам, которая и отличает серьезный код от студенческого, но это уже придирки.
    Для серьезного теста надо было разрешить более двух аргументов. Смысл в том, что для правильного вычисления выражений 2+3*4 без приоритетов операций уже не обойтись. Даже если кандидат и не уложится в полтора часа, важно, чтобы он шел по правильному пути.


    1. sens_boston
      31.01.2017 10:46

      Посмотрите мой пост чуть выше, касательно сложности подобного калькулятора на C# ;)
      Да, можно и с функциями (из System.Math), будет на несколько строчек длиннее.


      1. Ector
        31.01.2017 12:46

        Я видел, впечатляет! Но мне функционала DataTable.Compute хватить не могло: у меня при расчете значения выражения надо еще искать значения параметров с помощью обратного логического вывода (это экспертная система), да и функции тоже были специфические. А еще нужна работа с множествами.
        ИМХО, тут должно тестироваться не знание библиотеки, а понимание принципов обработки математических выражений. Хотя знать библиотеку тоже очень полезно: я вот не додумался до Compute…


        1. sens_boston
          31.01.2017 16:27

          Я же говорю, еще плюс несколько строк ;) Подскажу — постом ниже намек, с использованием runtime компиляции. Функционала C# (или VB, или Java script) вам ведь, наверное, где угодно хватило бы?

          А вот изобретать велосипеды нужно далеко не всегда, ну, и матчасть желательно знать…


        1. sens_boston
          31.01.2017 21:10

          Чтобы не быть голословным, вот слегка измененная программка на C#. Думаю, что за день можно было-бы доработать и до вашей задачи.

          C# string calculator with math support:
          //#define USE_DATATABLE
          using System;
          
          namespace Calculator
          {
              class Program
              {
                  static void Main(string[] args)
                  {
                      try
                      {
                          string expression = string.Join(" ", args).ToLower();
          #if USE_DATATABLE
                          Console.WriteLine(new System.Data.DataTable().Compute(expression, null));
          #else
                          Console.WriteLine(Eval(FixFunctionNames(expression)));
          #endif
                      }
                      catch (Exception error)
                      {
                          Console.WriteLine(error.Message);
                      }
                  }
          
                  static string FixFunctionNames(string expression)
                  {
                      foreach (System.Reflection.MethodInfo func in typeof(System.Math).GetMethods())
                      {
                          string funcName = func.GetBaseDefinition().Name.ToLower();
                          int funcPos = expression.IndexOf(funcName);
                          while (funcPos >= 0)
                          {
                              expression = expression.Insert(funcPos, "System.Math." + func.Name.Substring(0,1).ToUpper());
                              expression = expression.Remove(funcPos + 13, 1);
                              funcPos = expression.IndexOf(func.Name.ToLower(), funcPos+1);
                          }
                      }
                      return expression;
                  }
          
                  static object Eval(string expression)
                  {
                      try
                      {
                          return new Microsoft.CSharp.CSharpCodeProvider().CompileAssemblyFromSource(
                              new System.CodeDom.Compiler.CompilerParameters() { GenerateInMemory = true },
                              $"class Runtime{{public static object Eval(){{return {expression};}}}}"
                          ).CompiledAssembly.GetType("Runtime").GetMethod("Eval").Invoke(null, null);
                      }
                      catch
                      {
                          return "Error in expression: "+expression;
                      }
                  }
              }
          }
          


          1. Ector
            01.02.2017 07:22

            Увы, нельзя было. Моя задача — это экспертная система, часть большой PDM/PLM системы. Типичное выражение, которое я должен считать, примерно такое:
            Операция.Код? { 24424455, 24242566, 24244567 }
            При этом я должен найти атрибут Код от объекта типа «операция» (даже если текущий объект не операция — при этом работают мои правила поиска объектов), а потом сравнить его с одним из кодов из множества. И функции у меня тоже специфические. Но runtime compile мы используем тоже, правда для других задач. А «изобретать велосипеды» — это как раз то, что у меня получается лучше всего :) Хотя я и не спорю, что знать матчасть тоже нужно.


            1. sens_boston
              01.02.2017 09:12

              Я разработчик С#, которому пришлось писать настоящий калькулятор. То есть, со скобками, приоритетами операций и даже с функциями.

              Тогда непонятно, зачем вы упоминали калькулятор в своем посте. Код выше — это полноценный калькулятор, то есть, «со скобками, приоритетами операций и даже с функциями» :)
              Так, что вы были не правы, когда говорили, «это была работа точно не на полтора часа» (ну, или правы, так как работа по написанию подобного калькулятора на C# при определенном опыте — дело 10 минут).

              Что же до «смысла» такого теста, то это вопрос спорный. Скажем так: подобный тест при приеме на работу программиста начинающего/среднего уровня вполне нормальный, в принципе (правда, если не принимать во внимание, что у нас, в Штатах, обычно на собеседовании кодировать не просят; но могут попросить при screening), и уж намного лучше хитрожопых (сорри, но по иному назвать не могу!) вопросов «на IQ», или на «нестандартность мышления», или «а вот напиши мне quick sort на бумажке без компьютера», в общем, тех, которыми наши «милые» соотечественники, или нагуглившиеся индусы с китайцами любят «опускать» соискателя на собеседовании.

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

              Но, опять-таки, тут все очень сильно зависит от конкретной позиции (т.е. работы).


              1. Ector
                01.02.2017 10:07
                +1

                Моё начальство (не до конца понимающее, что делает моя система) считает это «калькулятором» :) Пользователи тоже так считают — они же не знают, как он устроен внутри. Простите, если ввел вас в заблуждение.
                Мне показалось, что замысел теста был в том, чтобы проверить, как кандидат САМ, без библиотек, напишет калькулятор. В этом есть немалый смысл: надо не только знать библиотеки, но и понимать, как они работают. Мы не раз требовали от кандидатов написать сортировку, и далеко не каждый с этим справился. Не вижу тут никакого «опускания». Если человек ошибся пару раз на единичку, это не страшно, а вот если он вообще не знает алгоритма — очень плохо.


                1. bamovetz
                  01.02.2017 11:18
                  -1

                  А вы ищете человека который вам будет сортировки писать?
                  Программист не должен помнить все и вся алгоритмы. Он должен уметь программировать. Хороший программист еще должен уметь искать и обрабатывать информацию. А Senior еще должен быстро входить в любую предметную область.


                  1. Ector
                    01.02.2017 11:41
                    -1

                    Мы не ищем человека, который нам будет сортировки писать. Но грамотный программист обязан знать, что делает сортировка, и сложность ее алгоритма — O(n*lg(n)). Никто не помнит все алгоритмы, но уж сортировка — это святое.
                    И, вы не поверите, но пару раз за мою более чем 20-летнюю карьеру мне все-таки пришлось писать сортировку самому.


                    1. sens_boston
                      01.02.2017 19:45

                      И что, прямо сейчас вот так вот возьмете, и карандашиком на листе бумаги (никакого гугла и stackoverflow) напишете quicksort или heapsort? Ну, если так, то вы реально «круты», поздравляю — если ваш код заработает, будучи перенесенным на компьютер :)

                      Что значит «без библиотек»? Библиотеки, те или иные, так или иначе используются. «Без библиотек» — это значит писать на ассемблере.

                      Программист, который не умеет reuse code (по-русски коряво выходит, извините), негодный программист. А вот кандидат, который умеет быстро, точно и эффективно решить задачу — подходящий кандидат.

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


                      1. Ector
                        02.02.2017 02:31

                        Вы не поверите, но когда я последний раз ходил на собеседование, меня попросили перечислить алгоритмы сортировки, которые я знаю. Просто перечислить. Я назвал с ходу шесть алгоритмов и сказал, что могу принести второй том Кнута :)
                        Разумеется, я не гарантирую, что код, который я напишу на бумажке, сразу будет без ошибок. Но уж алгоритмы сортировок-то я знаю. Как я уже говорил, неважно, если будут мелкие ошибки — важно, чтобы кандидат понимал суть алгоритма. Давным-давно, когда я был студентом, меня интересовали и доказательства правильности этих алгоритмов…
                        Я не собираюсь продолжать холивар на тему «что важнее демонстрировать на собеседовании — знание библиотек или алгоритмов». И то, и другое нужно. Но я считаю, что кандидат, плохо знающий библиотеку, обычно нагуглит недостающую информацию за пять минут, а вот кандидат, плохо знающий алгоритмы, завязнет в них надолго. И от него едва ли можно ожидать, например, выбора именно того алгоритма, который лучше всего подходит для вашей задачи.


                        1. sens_boston
                          02.02.2017 16:44

                          А «холивара» никакого и нет; я просто аргументированно возразил на ваше, неверное утверждение (о сложности такой задачи). Безусловно, и знание алгоритмов, и знание стандартных возможностей языков программирования и среды, и скорость решения задач весьма важны для разработчиков.

                          Просьба реализовать что-то не очень сложное на интервью, хоть и необычна (для нынешних реалий), и, возможно, «оскорбительна» для претендующих на senior-ов и team lead-ов, но вот для отсеивания начинающих неумех вполне подходит. Вдобавок, позволит понять, кого вы нанимаете, и что, в дальнейшем, вам ждать от человека.

                          Еще раз повторю, что, даже если бы вы нарисовали мне полностью рабочий quicksort на бумажке по памяти, один к одному из книжки Вирта, и рассказали бы даже то, чего я не знаю и не слышал никогда, но в ответ на просьбу написать калькулятор, заявили бы, что это задача на месяц, то работу вы бы не получили. Пусть я 100 раз не прав и «придираюсь», и вы немеряно круты и за год можете написать и отладить лексер с рекурсивным спуском.


  1. fogone
    31.01.2017 12:35

    fun calc(expression: String): BigDecimal {
        return "([-+]?\\d+)\\s*([+\\-*/])\\s*([-+]?\\d+)"
                .toRegex()
                .find(expression)
                ?.destructured
                ?.let { (left, op, right) ->
                    when (op) {
                        "+" -> BigDecimal::plus
                        "-" -> BigDecimal::minus
                        "*" -> BigDecimal::times
                        "/" -> BigDecimal::div
                        else -> throw IllegalArgumentException()
                    }(BigDecimal(left), BigDecimal(right))
                } ?: throw IllegalArgumentException()
    }
    
    fun main(args: Array<String>) {
        require(calc("+5 + -12") == BigDecimal(5 - 12))
        require(calc("+5 * -12") == BigDecimal(5 * -12))
        require(calc("+5 - -12") == BigDecimal(5 - (-12)))
        require(calc("+5 / -12") == BigDecimal(5).div(BigDecimal(-12)))
    }
    


    1. fogone
      02.02.2017 15:55

      А это пусть и не в одну строку, но зато понимает выражения.

      fun calc(expression: String): BigDecimal {
          val ast = simpleParser().parse(StringReader(expression))
          return ast?.evaluate(EmptyEvaluationContext()) ?: throw IllegalArgumentException()
      }
      
      fun main(args: Array<String>) {
          require(calc("(5 + -14) - 23") == BigDecimal((5 + (-14)) - 23))
          require(calc("((5 + -14) - 23) + (-10 * 6)") == BigDecimal(((5 + -14) - 23) + (-10 * 6)))
      }
      


      1. dougrinch
        02.02.2017 17:16
        -1

        Вы невнимательно читали условие, это решение вообще без строк.


        всего 1 строку, т.е. всего 1у точку с запятой не считая пакеты и импорты


        1. fogone
          02.02.2017 18:35

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


  1. kishchenko
    31.01.2017 15:01

    Для С# с возможностью вычисления сложных выражений.

            static void Main(string[] args)
            {
                Console.WriteLine(Eval("(5 * 5) - 2"));
            }
    
            static object Eval(string expression)
            {
                return new CSharpCodeProvider().CompileAssemblyFromSource(
                    new CompilerParameters() { GenerateInMemory = true },
                    $"class Runtime{{public static object Eval(){{return {expression};}}}}"
                ).CompiledAssembly.GetType("Runtime").GetMethod("Eval").Invoke(null, null);
            }
    


  1. disputant
    31.01.2017 15:13

    Держите на С++. Точка с запятой одна. Защиту от дурака никто не требовал :)

    double calc(const char * s, char c = '+', double a = 0.0, double b = 0.0)
    {
        return (sscanf(s,"%lf %c %lf",&a,&c,&b) == 3) ? ((c == '+') ? (a+b) : (c == '-') ? (a-b) : (c == '*') ? (a*b) : (a/b)) : 0.0;
    }
    
    int main()
    {
        cout << calc("-3.154+18") << endl;
        cout << calc("-3.154 +-18") << endl;
    }
    


  1. Pusk1
    31.01.2017 15:16

    В моей школьной юности это была задачка для строковых операций на олимпиаде. Правда, с приоритетом операций и скобками. Как сейчас помню, что перевёл всё в польскую запись, а потом уже посчитал. Тогда минут в 50 уложился. Сейчас пол дня уйдёт:) Правда, код редко пишу.


  1. bamovetz
    31.01.2017 18:04

    Я с подобных собеседований ухожу сразу после фразы «Удиви меня».
    Мы не на соревновании.


  1. Chaptykov
    01.02.2017 04:21

    Кажется, в комментариях слишком мало странных вариантов c очень ограниченной функциональностью, написанных с использованием JS.


    (s=>!(document.body.style.width='calc('+s.replace(/(\d+)/g,'$&px')+')')||document.body.offsetWidth)('5 + 45');


  1. molecularmanvlz
    01.02.2017 09:45
    +1

    public static void main(String[] args) {
            System.out.println(calc("+5 + -12").equals("-7.0"));
            System.out.println(calc("+5 * -12").equals("-60.0"));
            System.out.println(calc("+5 - -12").equals("17.0"));
            System.out.println(calc("+5 / -12").equals("-0.4166666666666667"));
            System.out.println(calc("+5 & -12").equals("unknown operator"));
            System.out.println(calc("-12").equals("invalid expression"));
        }
    
        private static String calc(String expr) {
            return expr.split(" ").length > 2 ?
                   expr.split(" ")[1].equals("+") ? "" + (Double.valueOf(expr.split(" ")[0]) + Double.valueOf(expr.split(" ")[2])) :
                   expr.split(" ")[1].equals("-") ? "" + (Double.valueOf(expr.split(" ")[0]) - Double.valueOf(expr.split(" ")[2])) :
                   expr.split(" ")[1].equals("*") ? "" + (Double.valueOf(expr.split(" ")[0]) * Double.valueOf(expr.split(" ")[2])) :
                   expr.split(" ")[1].equals("/") ? "" + (Double.valueOf(expr.split(" ")[0]) / Double.valueOf(expr.split(" ")[2])) : "unknown operator" : "invalid expression";
        }
    


  1. ApeCoder
    02.02.2017 00:19

    F# но не поддерживает отрицаетльные числа


    open System.Text.RegularExpressions 
    let calc s = 
        Regex.Split(s, @"\s*([\d\.]+)\s*") 
            |> Seq.chunkBySize 2       
            |> Seq.filter (Array.length >> (=) 2)
            |> Seq.fold (fun acc [|op; no|] -> 
                ([("-", (-)); ("+", (+)); ("*", (*)); ("/", (/)); ("", (+))] |> Map.ofSeq |> Map.find op) acc (float no)
            ) 0.
    
    [<EntryPoint>]
    let main argv = 
        printfn "%A" (calc "2+2")
        printfn "%A" (calc "2*3.5 + 8")
        0 // возвращение целочисленного кода выхода


  1. titsi
    04.02.2017 15:10

    В книженции по алгоритмам был код на основе двухстекового алгоритма Дейкстры:

    public class Evaluate {
        public static void main(String[] args){
            Stack<String> ops = new Stack<String>();
            Stack<Double> vals = new Stack<Double>();
            while(!StdIn.isEmpty()){
                String s = StdIn.readString();
                //System.out.println(s);
                if(s.equals("("));
                else if(s.equals("+")) ops.push(s);
                else if(s.equals("-")) ops.push(s);
                else if(s.equals("*")) ops.push(s);
                else if(s.equals("/")) ops.push(s);
                else if(s.equals("sqrt")) ops.push(s);
                else if(s.equals(")"))
                {
                    //выталкиваем элемент вычисляем и вталкиваем результат
                    String op = ops.pop();
                    double v = vals.pop();
                    if ( op.equals("+"))    v = vals.pop() + v;
                    else if (op.equals("-")) v = vals.pop() - v;
                    else if (op.equals("*")) v = vals.pop() * v;
                    else if (op.equals("/")) v = vals.pop() / v;
                    else if (op.equals("sqrt")) v = Math.sqrt(v);
                    vals.push(v);
                } //элемент не операция и не скобка значит вталкиваем
                //значение double
                else vals.push(Double.parseDouble(s));
            }
            StdOut.println(vals.pop());
        }
    }
    
    

    Если ввести в консоль
    ( ( 1 + sqrt ( 5.0 ) ) / 2.0 )
    то напишет
    1,61803…


    1. zip_zero
      07.02.2017 10:51

      Прекрасно!
      Лучшая иллюстрация пользы от изучения классических алгоритмов.