Консольный калькулятор v1.0
Мама, я стьюдент-пре-джуниор разработчик.
Введение
Не в первый раз я решаюсь начать учить программирование в личных и профессиональных целях, но в этот раз я взялся за это дело серьёзно. В начале лета 2023 я поставил перед собой цель — в конце лета я должен знать основы, и в свободное время начал изучать C# по курсу на ULearn.me. Для общего развития в моих рекомендациях на YouTube появились видео о информатике.
Я считаю, что при обучении чему-то новому нужно как можно скорее приступать применять полученные знания на практике. Это помогает не утонуть в теории, поддерживать постоянный интерес к обучению и просто по человечески приятно видеть результаты по ходу своей работы (я так бельё глажу, гораздо легче, когда у тебя не просто уменьшается одна общая куча, а исчезают по одной много маленьких, отсортированных по какому-то признаку).
Как применять знания на практике? Конечно же делать проекты, от простых к сложным, через тернии к звёздам. В целом, проектный подход мне близок, не только из-за того, что это мои непосредственные профессиональные компетенции, но и потому что я считаю, что проекты окружают нас повсюду. Ремонт, отпуск, поход в магазин, свидание, прохождение обучающего курса, написание статьи и т. п. — это проекты и мыслить в жизни нужно проектно. Из одной книжки помню примерно такую цитату: «профессиональная карьера — это череда проектов», я бы её дополнил до: «жизнь — это череда проектов».
С таким настроем я приступил к этому проекту, и конечно же, забыл об основах проектного менеджмента, начал сразу с кодинга, минуя планирование и проектирование, о чём в дальнейшем пожалел.
Что хотел сделать
Я сразу понимал, что хотел сделать калькулятор, где всё было бы написано в одной строке, чтобы не было «многострочности», то есть такого:
Enter first number: 2 Enter second: 2 Enter operation: + Answer: 4
Во-первых, это не красиво, во-вторых, я всё-таки планирую пользоваться своим приложением, а постоянно отбивать Enter просто не удобно. Писать приложение ради того чтобы написать — не мой подход.
Кроме «однострочности», я хотел сделать хоть какие-то настройки для солидности проекта. И людям не стыдно показать, если есть настройки — приложение серьёзное. Пока я точно не знал какие это будут настройки, но они должны быть.
Приложение должно корректно отрабатывать крайние значения (например, деление на ноль), выводить предупреждения если в строке не валидные данные и выполнять ряд проверок. Одним словом, оно должно быть стабильным и иметь защиту от дурака.
- Приложение должно быть юзерфрендли
- В приложении должны быть настройки
- В приложении должна присутствовать защита от дурака
Что сделал
Первый подход к снаряду
Исходя из принципа от простого к сложному я начал делать «многострочный» калькулятор, в моих планах было получить работающее приложение и уже по ходу дела доводить его до того состояния, которое я себе представлял.
В начале, конечно, всё шло гладко и в режиме «многострочности», и даже в режиме «однострочности» приложение корректно отрабатывало все тесты. Проблемы начались тогда, когда я понял, что люди калькулятором так не пользуются, точнее пользуются только в момент первого ввода или после отчистки памяти. Мне, и я предполагаю, что любому пользователю калькулятора, естественно второй и последующие разы вводить только оператор и следующее число, пока не отчисться память, то есть примерно так:
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 остальные:
- Проверка отсутствие оператора в строке. Конструкцию поиска arrOperation.Any (str.Contains) через документацию по C# найти мне не удалось, здесь я прибегнул к помощи Алисы и YaGPT 2, подробнее об этом ниже.
- Проверка деление на ноль. Как не странно, сначала не было условия ИЛИ, и только в ходе тестов, я понял, что не могу делить на числа от 0 до 1.
- Проверка на запрещённые символы в строке. Долго не хотел делать массив arrBanSymbol, в моей голове была установка, что можно как-то с помощью языковых конструкций проверять, что в строке есть только цифры и математические операторы. Поиски мои не увенчались успехов и в коде появился массив с запрещёнными символами.
- Проверка отсутствие чисел в строке. Зеркальная первой в списке проверка, есть оператор, но нет числа.
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 я буду называть Алисой, имеется ввиду общение с ней именно в этом навыке.
Решил специально не использовать каких-то конструкций (кроме «С#» вначале), чтобы переписка максимально походила на то, как я бы задавал вопрос опытному разработчику.
Конечно я также пытался найти ответы и на те моменты, которые у меня не получилось реализовать в программе.
Предательски Алиса выдаёт другой ответ на момент записи экрана, когда я писал код информацию о ValueTuple она не выводила. Надо будет изучить.
Внезапно Алиса перестала понимать о чем идёт речь. Сетую на то, что мой запрос для ней выглядит странно, но человеку я задал вопрос бы именно так.
После попробовал вставить кусок кода побольше для того чтобы появился контекст, ответ забавный.
Опыт поиска информации через Yandex GPT 2 определённо положительный. Искал я, конечно, гораздо больше информации через Алису и вопросы были более глупыми.
Если не удаётся найти информацию в документации, я щёл со своим запросом к Алисе, плюс у неё можно после попросить ссылки на источники.
GitHub
Известен не только в узких кругах, я думаю, что каждый, кто проведёт достаточно времени в интернете хотя бы раз услышит что-то о GitHub, и я не исключение.
Завершённый проект или итерация проекта должны быть доступны для скачивания, доработки и комментирования другими людьми. Обратная связь — неотъемлемая часть обучения, критически необходимо производить общение с более опытными коллегами, так я и решился создать аккаунт на GitHub.
Просмотрев информацию, что вообще такое Git (про Hub более менее понятно), я понял что меня ждёт куча кнопок с функционалом, который мне не нужен, а это значит запутаться и кликнуть не туда будет очень просто. Благо со второй попытки получилось опубликовать репозиторий, и теперь с гордостью могу сказать, что исходный код моего калькулятора вы можете скачать на GitHub.
Многое на площадке осталось не понятным, надеюсь со временем разберусь. Почему в релиз попал только исходный код без скомпилированного приложения я так и не понял. По этому для простых обывателей, без Visual Studio, по этой ссылке можно скачать уже само приложение.
Выводы
Проект без выводов - не проект.
Радости полные штаны от реализованного проекта, ещё больше рад, что в ходе создания такого простого приложения познакомился с GitHub и YaGPT 2. Я считаю, что задачи выполнены, приложение можно использовать. Осталось добавить его в быстрый доступ и использовать по максимуму.
Я на 99% уверен, что написанный мной код может быть улучшен, какие-то части можно написать более эффективно, а может быть и всё приложение. Но на текущий момент я доволен тем, что написал и как это работает, в будущем возможны доработки.
Если вам было интересно читать статью о самом начале пути разработчика, подписывайтесь! Как наберётся материал, подобных статей будет больше. Рад комментариям и обсуждениям по теме, ссылки на профильные ресурсы для просмотра и чтения приветствуются.