Код
September 21, 2023

Консольный калькулятор v1.0

Мама, я стьюдент-пре-джуниор разработчик.


Введение

Не в первый раз я решаюсь начать учить программирование в личных и профессиональных целях, но в этот раз я взялся за это дело серьёзно. В начале лета 2023 я поставил перед собой цель — в конце лета я должен знать основы, и в свободное время начал изучать C# по курсу на ULearn.me. Для общего развития в моих рекомендациях на YouTube появились видео о информатике.

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

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

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

Прогресс на момент начала проекта

Что хотел сделать

Я сразу понимал, что хотел сделать калькулятор, где всё было бы написано в одной строке, чтобы не было «многострочности», то есть такого:

Enter first number: 2
Enter second: 2
Enter operation: +
Answer: 4

Во-первых, это не красиво, во-вторых, я всё-таки планирую пользоваться своим приложением, а постоянно отбивать Enter просто не удобно. Писать приложение ради того чтобы написать — не мой подход.

Кроме «однострочности», я хотел сделать хоть какие-то настройки для солидности проекта. И людям не стыдно показать, если есть настройки — приложение серьёзное. Пока я точно не знал какие это будут настройки, но они должны быть.

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

Подытожим задачи проекта:

  1. Приложение должно быть юзерфрендли
  2. В приложении должны быть настройки
  3. В приложении должна присутствовать защита от дурака

Что сделал

Первый подход к снаряду

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

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

12+12            //первый ввод
24               //ответ
-2
20               //ответ
/36,745
0,54429174037    //ответ

Но из-за того что я назвал переменные firstNumber и secondNumber, я запутался в собственном коде и не смог перестроить его на логику предыдущего и текущего числа. Знаю, звучит глупо, но для меня это было фатально, было решено сделать Ctrl+A → Del и на следующий день, с чистой головой, нарисовать алгоритм и только потом кодить.

Записей кода, к сожалению, не осталось, показать нечего.

Второй подход к снаряду

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

Первый вариант алгоритма

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

public static string GetString()
{
    var imputString = Console.ReadLine();
    while (String.IsNullOrEmpty(imputString))
    {
        Console.WriteLine("WARNING: Strig is empty (in memory " + previousNubmber + ")");
        imputString = Console.ReadLine();
    }
    return imputString;
}

Следующим на очереди был блок определения оператора. Есть массив, находим индекс любого элемента массива в строке, вычленяем подстроку с указанным индексом и длинной. Сейчас звучит всё просто, но для того чтобы найти способ поиска любого элемента массива в строке пришлось потратить не мало времени, сказывается неопытность в работе с документацией C#, но в итоге нашёл IndexOfAny и был рад. Ближе к концу проекта я научился задавать корректные поисковые запросы для более релевантного поиска специфической информации.

str = GetString();
char[] arrOperation = { '+', '-', '*', '/', '^' };

//find operation
var operationIndex = str.IndexOfAny(arrOperation);
var operation = str.Substring(operationIndex, 1);

Прежде чем приступить к единственному ромбику в алгоритме я понимал, что сами математические операции логичнее всего выделить в отдельный метод, и уже этот метод вызывать в ромбике. Так появился метод MathMagic. Про округление возвращаемого значения до roundValue чуть попозже, а сейчас хочется сказать, что синтаксический сахар C# действительно сахар. If без фигурных скобок — красота, для моего дилетантского, в теме кодинга, глаза. Я ещё не слишком силён во всех всплывающих подсказках в Visual Studio и в целом возможностях IDE, но когда после написание первого «else» программа предложила написать код дальше я бы очень удивлён. Факт, что Visual Studio понимает намерение пользователя — это что-то из области фантастики.

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

public static double MathMagic(double x, double y, string o)
{
    var ans = 0d; //на сколько разобрался, это лучше чем "double ans = 0;"
    if (o == "+")
        ans = x + y;
    else if (o == "-")
        ans = x - y;
    else if (o == "*")
        ans = x * y;
    else if (o == "/")
        ans = x / y;
    else
        ans = Math.Pow(x, y);
    return Math.Round(ans, roundValue);
}

Теперь можно полноценно говорить и о ромбике. Он реализован на простой проверке индекса оператора, который мы уже определили. Глобальные переменные previousNumber и curentNumber парсятся из строки, полученной методом GetString, вычисляется MathMagic и на экран выводиться ответ равный previousNumber.

Спойлер: CultureInfo.InvariantCulture нормально не работает.

if (operationIndex == 0)
{
    //not first enter
    curentNumber = double.Parse(str.Substring(operationIndex + 1), CultureInfo.InvariantCulture);

    previousNubmber = MathMagic(previousNubmber, curentNumber, operation);
    Console.WriteLine(previousNubmber);
}
else
{
    //first enter
    previousNubmber = double.Parse(str.Substring(0, operationIndex), CultureInfo.InvariantCulture);
    curentNumber = double.Parse(str.Substring(operationIndex + 1), CultureInfo.InvariantCulture);

    var output = MathMagic(previousNubmber, curentNumber, operation);
    previousNubmber = output;
    Console.WriteLine(previousNubmber);
}

Осталось всё засунуть в бесконечный цикл for и функционал в первой итерации готов, выглядит примерно так.

public static double previousNubmber = 0;
public static double curentNumber = 0;
public static int roundValue = 6;

public static void Main() 
{
    var str = "";
    for (; ;)
    {
        //получение строки
        //определение оператора
        //математика
    }
}

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

Рефакторинг

Приложение работает, но в задачах проекта остались ещё два пункта, начнём с настроек.

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

Для начала необходимо в принципе определить, что данные в строке — это настройка. Из моего скромного опыта работы с консольными приложениями от профессиональных разработчиков я знаю, что команды в строке выглядят как «-r» или «--round», честно сказать, почему так я не знаю, но решил сделать также. Между получением строки и определением оператора в бесконечном цикл for вставил проверку строки с помощью StartsWith (нашел уже гораздо быстрее чем IndexOfAny).

Суть в том, что вызывается метод округления RoundForAnswer, в строку выводиться релевантная информация и цикл for начинается заново.

//input round value
if (str.StartsWith("-r") || str.StartsWith("--round"))
{
    roundValue = RoundForAnswer(str);
    Console.WriteLine("Now rounds the value to " + roundValue + " decimal places, (in memory " + previousNubmber + ")");
    continue;
}

Сам метод прост как две копейки, в возвращаемое значение парситься подстрока с указанного символа.

public static int RoundForAnswer(string s)
{
    var value = roundValue;
    if (s.StartsWith("-r"))
        value = int.Parse(s.Substring(3));
    else if (s.StartsWith("--round"))
        value = int.Parse(s.Substring(8));
    return value;
}

Предполагаю, что метод будет вызываться только с релевантными строковыми данными (то есть «-r» или «--round» точно есть в строке). Вероятно, при работе в команде в таких случаях нужно предусматривать или проверку данных, или детально описывать метод в ///<summary>.

Таким образом, приложение стало серьёзнее в нём появились настройки, настраивать можно один параметр. Я доволен.

Если есть команда, значит должна быть и справка к этой команде. Похожим способом я определяю, что в строку ввели «-h» или «--help», после чего выводиться таблица с доступными командами и ещё решил добавить информацию о доступных операторах. На моё удивление \t сработал так как я себе представлял, он не просто ставит табуляцию, а ещё и выравнивает таблицу.

Console.WriteLine("[Help]");
Console.WriteLine("Comands\n" + 
                  "-c \t --cancel \t Cancel/clear memory\n" +
                  "-h \t --help \t Help\n" +
                  "-i \t --info \t Application information\n" +
                  "-r \t --round \t Round value (default is 6 decimal places) [-r 4 -> 4 decimal places]\n" +
                  "\nOperation\n" +
                  "+ \t Plus operation\n" +
                  "- \t Minus operation\n" +
                  "* \t Multiplication operation\n" +
                  "/ \t Divide operation\n" +
                  "^ \t Exponentiation operation\n");
                  

Настало время реализации защиты от дурака. В приложении есть 5 проверок, одну из них мы уже видели, это проверка на пустую строку, в этом блоке реализованы 4 остальные:

  1. Проверка отсутствие оператора в строке. Конструкцию поиска arrOperation.Any (str.Contains) через документацию по C# найти мне не удалось, здесь я прибегнул к помощи Алисы и YaGPT 2, подробнее об этом ниже.
  2. Проверка деление на ноль. Как не странно, сначала не было условия ИЛИ, и только в ходе тестов, я понял, что не могу делить на числа от 0 до 1.
  3. Проверка на запрещённые символы в строке. Долго не хотел делать массив arrBanSymbol, в моей голове была установка, что можно как-то с помощью языковых конструкций проверять, что в строке есть только цифры и математические операторы. Поиски мои не увенчались успехов и в коде появился массив с запрещёнными символами.
  4. Проверка отсутствие чисел в строке. Зеркальная первой в списке проверка, есть оператор, но нет числа.
char[] arrOperation = { '+', '-', '*', '/', '^' };
char[] arrBanSymbol = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~!@#$%&()_=`';:'".ToCharArray(); //for check
//str check
if (arrOperation.Any(str.Contains) == false) //check for arrOperation in str
{
    Console.WriteLine("WARNING: String without operation (in memory " + previousNubmber + ")");
    continue;
}
else if (str.Contains("/0") && str.Contains("/0.") != true) //check for divide by zero
{
    Console.WriteLine("WARNING: Divide by zero (in memory " + previousNubmber + ")");
    continue;
}
else if (arrBanSymbol.Any(str.Contains)) //check for arrBanSymbol in str
{
    Console.WriteLine("WARNING: String contains ban symbols (in memory " + previousNubmber + ")");
    continue;
}
else if (arrOperation.Any(str.Contains) && str.Length == 1) //check for str with arrOperation, but without numbers
{
    Console.WriteLine("WARNING: String without numbers (in memory " + previousNubmber + ")");
    continue;
}

Итог

В итоге алгоритм эволюционировал до такого.

Финальный алгоритм

Основной цикл приложения выглядит примерно так.

public static double previousNubmber = 0;
public static double curentNumber = 0;
public static int roundValue = 6;

public static void Main() 
{
    var str = "";
    for (; ;)
    {
        //получение строки
        //команды
        //проверка данных в строке
        //определение оператора
        //математика
    }
}

Что не получилось

Ввод с разных раскладок

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

Клавиша точки на нампаде на разных раскладах ставит разный символ: на английской будет «12.34+56.78», а на русской «12,34+56,78»; и вроде как всё просто решается простым CultureInfo.InvariantCulture, но в моём случае он готовит данные под тип double, а не даёт возможность использовать и точку и запятую.

using System.Globalization;

previousNubmber = double.Parse(str.Substring(0, operationIndex), CultureInfo.InvariantCulture);
curentNumber = double.Parse(str.Substring(operationIndex + 1), CultureInfo.InvariantCulture);

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

Копирование ответа в буфер обмена

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

Я конечно изначально это не планировал, это родилось само собой, по ходу тестирования и использования (agile в чистом виде). Реализация этой фичи полностью бы удовлетворила задачу удобства использования, но этому не суждено было сбыться. Все поиски по этой теме вели меня на классы System.Windows.Clipboard или System.Windows.Forms, которые в .NET 6.0 просто отсутствуют. Уверен, что это можно сделать, но пока я не знаю как. Опять же таки из логики, что лучшее — враг хорошего, я оставил поиски, а фича осталась не реализованной.

YandexGPT 2

Поиск информации — это важная часть обучения, в силу своей неопытности искать в поиске по документации C# у меня получается не так хорошо как хотелось бы. На помощь приходят нейросети, я решил попробовать найти необходимую мне информацию в YaGPT 2, которую недавно выпустил Яндекс. Мне понравилось.

Далее YaGPT 2 я буду называть Алисой, имеется ввиду общение с ней именно в этом навыке.

Решил специально не использовать каких-то конструкций (кроме «С#» вначале), чтобы переписка максимально походила на то, как я бы задавал вопрос опытному разработчику.

Поиск arrOperation.Any(str.Contains)

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

Попытки найти способ копировать значение в буфер обмена

Предательски Алиса выдаёт другой ответ на момент записи экрана, когда я писал код информацию о ValueTuple она не выводила. Надо будет изучить.

Внезапно Алиса перестала понимать о чем идёт речь. Сетую на то, что мой запрос для ней выглядит странно, но человеку я задал вопрос бы именно так.

Потеря контакта

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

Да, это код на C#

Опыт поиска информации через Yandex GPT 2 определённо положительный. Искал я, конечно, гораздо больше информации через Алису и вопросы были более глупыми.

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

Логический переход к следующему блоку

GitHub

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

Завершённый проект или итерация проекта должны быть доступны для скачивания, доработки и комментирования другими людьми. Обратная связь — неотъемлемая часть обучения, критически необходимо производить общение с более опытными коллегами, так я и решился создать аккаунт на GitHub.

Просмотрев информацию, что вообще такое Git (про Hub более менее понятно), я понял что меня ждёт куча кнопок с функционалом, который мне не нужен, а это значит запутаться и кликнуть не туда будет очень просто. Благо со второй попытки получилось опубликовать репозиторий, и теперь с гордостью могу сказать, что исходный код моего калькулятора вы можете скачать на GitHub.

Многое на площадке осталось не понятным, надеюсь со временем разберусь. Почему в релиз попал только исходный код без скомпилированного приложения я так и не понял. По этому для простых обывателей, без Visual Studio, по этой ссылке можно скачать уже само приложение.

Выводы

Проект без выводов - не проект.

Радости полные штаны от реализованного проекта, ещё больше рад, что в ходе создания такого простого приложения познакомился с GitHub и YaGPT 2. Я считаю, что задачи выполнены, приложение можно использовать. Осталось добавить его в быстрый доступ и использовать по максимуму.

Я на 99% уверен, что написанный мной код может быть улучшен, какие-то части можно написать более эффективно, а может быть и всё приложение. Но на текущий момент я доволен тем, что написал и как это работает, в будущем возможны доработки.

Возможные планы на улучшение:

  • Реализация нескольких операторов в одной строке
  • Реализация функционала скобок
  • Тригонометрия

Если вам было интересно читать статью о самом начале пути разработчика, подписывайтесь! Как наберётся материал, подобных статей будет больше. Рад комментариям и обсуждениям по теме, ссылки на профильные ресурсы для просмотра и чтения приветствуются.