diff --git a/.gitignore b/.gitignore index 5a62da38..c8b9f4e7 100644 --- a/.gitignore +++ b/.gitignore @@ -454,4 +454,3 @@ $RECYCLE.BIN/ !.vscode/extensions.json /Calculator.TomDonegan/TextFile1.txt /Calculator.TomDonegan/Calculator.TomDonegan/Notes.txt -/Calculator diff --git a/Calculator/.gitattributes b/Calculator/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/Calculator/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/Calculator/.gitignore b/Calculator/.gitignore new file mode 100644 index 00000000..9491a2fd --- /dev/null +++ b/Calculator/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/Calculator/Calculator.csproj b/Calculator/Calculator.csproj new file mode 100644 index 00000000..43d5e148 --- /dev/null +++ b/Calculator/Calculator.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/Calculator/Calculator.slnx b/Calculator/Calculator.slnx new file mode 100644 index 00000000..9d6af919 --- /dev/null +++ b/Calculator/Calculator.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/Calculator/CalculatorLibrary/CalculatorLibrary.cs b/Calculator/CalculatorLibrary/CalculatorLibrary.cs new file mode 100644 index 00000000..d3d2ddf5 --- /dev/null +++ b/Calculator/CalculatorLibrary/CalculatorLibrary.cs @@ -0,0 +1,97 @@ +namespace CalculatorLibrary; + +public class Calculator +{ + public Log CalcLog { get; set; } = new (); + + public Calculator () + { + CalcLog.Load (); + + ++CalcLog.UsedCount; + CalcLog.Save (); + } + + public string DoOperation (double num1, double num2, string op) + { + double val = double.NaN; + + switch (op) + { + case "a": + val = num1 + num2; + break; + + case "s": + val = num1 - num2; + break; + + case "m": + val = num1 * num2; + break; + + case "d": + if (num2 != 0) + val = num1 / num2; + break; + + case "sqrt": + val = Math.Sqrt (num1); + break; + + case "pow": + if (num1 != 0 || num2 != 0) + val = Math.Pow (num1, num2); + + break; + + case "10": + val = Math.Pow (10, num1); + break; + + case "e": + val = Math.Exp (num1); + break; + + case "sin": + val = Math.Sin (ToRadian (num1)); + break; + + case "cos": + val = Math.Cos (ToRadian (num1)); + break; + + case "tg": + num1 = Reduce (num1); + if (Math.Abs (num1) != 90) + val = Math.Tan (ToRadian (num1)); + break; + + case "ctg": + num1 = Reduce (num1); + if (Math.Abs (num1) != 0) + val = 1.0 / Math.Tan (ToRadian (num1)); + break; + } + + if (double.IsNaN (val) || val == double.NegativeInfinity || val == double.PositiveInfinity) + return string.Empty; + + // To prevent '-0' in result + if (Math.Abs (val) == 0) + val = 0; + + return string.Format ("{0:0.######}", val); + } + + double ToRadian (double degree) + { + return degree * Math.PI / 180.0; + } + + // Converts degrees > 180 or < -180 in degrees from interval (-180, 180) + double Reduce (double degree) + { + return degree % 180; + } +} diff --git a/Calculator/CalculatorLibrary/CalculatorLibrary.csproj b/Calculator/CalculatorLibrary/CalculatorLibrary.csproj new file mode 100644 index 00000000..22badd14 --- /dev/null +++ b/Calculator/CalculatorLibrary/CalculatorLibrary.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/Calculator/CalculatorLibrary/Gui.cs b/Calculator/CalculatorLibrary/Gui.cs new file mode 100644 index 00000000..59cb3dd3 --- /dev/null +++ b/Calculator/CalculatorLibrary/Gui.cs @@ -0,0 +1,33 @@ +namespace CalculatorLibrary; +public class Gui +{ + public static void WriteColorText (string text, ConsoleColor colorFoderground, int colorBackground = -1) + { + ConsoleColor curColorForeground = Console.ForegroundColor; + ConsoleColor curColorBackground = Console.BackgroundColor; + + Console.ForegroundColor = colorFoderground; + if (colorBackground >= 0) + Console.BackgroundColor = (ConsoleColor) colorBackground; + + Console.Write (text); + + Console.ForegroundColor = curColorForeground; + Console.BackgroundColor = curColorBackground; + } + + // Notify about some exeption or give some feedback on console. The text will be dissapeared after 2 seconds + public static void Notify (string notification, ConsoleColor colorFoderground = ConsoleColor.White, ConsoleColor colorBackground = ConsoleColor.DarkRed) + { + var (x, y) = Console.GetCursorPosition (); + Console.CursorVisible = false; + + WriteColorText (notification, colorFoderground, (int) colorBackground); + System.Threading.Thread.Sleep (2000); + + Console.SetCursorPosition (x, y); + Console.Write (new string (' ', notification.Length)); + Console.SetCursorPosition (x, y); + Console.CursorVisible = true; + } +} diff --git a/Calculator/CalculatorLibrary/Log.cs b/Calculator/CalculatorLibrary/Log.cs new file mode 100644 index 00000000..dff3330a --- /dev/null +++ b/Calculator/CalculatorLibrary/Log.cs @@ -0,0 +1,81 @@ + +using Newtonsoft.Json; + +namespace CalculatorLibrary; + +// Date of one calcualtion to store in log +public class CalcData +{ + public string Input { get; set; } // Input in format "operand1 opeartion operand2", i.e. "2 x 3" + public string Result { get; set; } + + public CalcData (string input, string result) + { + Input = input; + Result = result; + } +} + +public class Log +{ + public int UsedCount { get; set; } // Count of launched times + + public List Calculations { get; set; } = new (); + + readonly string nameLogFile = "log.json"; + + public void Load () + { + try + { + using (StreamReader reader = new (nameLogFile)) + { + string json = reader.ReadToEnd (); + + Log? log = JsonConvert.DeserializeObject (json); + if (log != null) + { + UsedCount = log.UsedCount; + Calculations = log.Calculations; + } + } + } + catch (Exception) + { + // No information about previous calculations available + } + } + + public void Save () + { + try + { + string json = JsonConvert.SerializeObject (this, formatting: Formatting.Indented); + using (StreamWriter writer = File.CreateText (nameLogFile)) + { + writer.Write (json); + } + } + catch (Exception) + { + } + } + + public void AddInfo (string input, string result) + { + Calculations.Add (new (input, result)); + Save (); + } + + public void Delete (int index) + { + Calculations.RemoveAt (index); + Save (); + } + + public void Reset () + { + Calculations.Clear (); + Save (); + } +} diff --git a/Calculator/CalculatorLibrary/LogList.cs b/Calculator/CalculatorLibrary/LogList.cs new file mode 100644 index 00000000..21e84766 --- /dev/null +++ b/Calculator/CalculatorLibrary/LogList.cs @@ -0,0 +1,333 @@ +namespace CalculatorLibrary; + +// Represent one line in calculations list +public class CalcLine +{ + public int PosX { get; set; } + + public int PosY { get; set; } + + public string Text { get; set; } + + public CalcLine (int posX, int posY, string text) + { + PosX = posX; + PosY = posY; + Text = text; + } + + public void Display (bool selected = false) + { + var colorForeground = Console.ForegroundColor; + var colorBackground = Console.BackgroundColor; + if (selected) + { + Console.ForegroundColor = ConsoleColor.Black; + Console.BackgroundColor = ConsoleColor.White; + } + + Console.SetCursorPosition (PosX, PosY); + Console.Write (Text); + + Console.ForegroundColor = colorForeground; + Console.BackgroundColor = colorBackground; + } +} + +// List with previous calculations +public class LogList +{ + Log _log; + List _lines = new (); + + int _selectedIndex; // Index of selected line + int _viewIndex; // Index of first visible list's item + int _posYMin; // Y-position of first viisble item in list + int _posYMax; // Y-position of last visible item in list + + readonly int _maxLineCount = 8; // Maximal count of lines in list + readonly int _maxWidth = 40; // Maximal width of list in symbols + + public LogList (Log log) + { + _log = log; + } + + public string Show (out double val) + { + val = double.NaN; + + if (_log.Calculations.Count <= 0) + { + Gui.Notify (" There is no calculations yet ", ConsoleColor.Black, ConsoleColor.White); + return string.Empty; + } + + _lines.Clear (); + + string line = new ('─', _maxWidth); + string indent = " "; + Console.WriteLine (" Previous calculations"); + Console.WriteLine ("{0}┌{1}┐", indent, line); + + var (x, y) = Console.GetCursorPosition (); + x += indent.Length + 1; + _posYMin = y; + _posYMax = y + _maxLineCount; + + // Prepare view of list's items to display + string text = string.Empty; + foreach (var data in _log.Calculations) + { + text = $" {data.Input} = {data.Result}"; + if (text.Length > _maxWidth) + { + string subText = text.Substring (0, _maxWidth - 3); + text = subText + "..."; + } + else + text = text.PadRight (_maxWidth, ' '); + + _lines.Add (new (x, y++, text)); + } + + // Show empty list + text = new (' ', _maxWidth); + for (int i = 0; i < _maxLineCount; ++i) + Console.WriteLine ($"{indent}│{text}│"); + + Console.WriteLine ("{0}└{1}┘\n", indent, line); + + Tuple [] commands = + [ + Tuple.Create ("\tUp", " go one line up "), + Tuple.Create ("Down", " go one line down"), + Tuple.Create ("\tPgUp", " go to first record "), + Tuple.Create ("PgDown", " go to last record"), + Tuple.Create ("\tDel", " delete current record "), + Tuple.Create ("R", " reset list content"), + Tuple.Create ("\tB", " back to calculation "), + Tuple.Create ("E", " exit program"), + Tuple.Create ("\tEnter", " use result of current record for callculation") + ]; + + for (int i = 0; i < commands.Length; ++i) + { + var (key, description) = commands [i]; + Gui.WriteColorText (key, ConsoleColor.Cyan); + Console.Write (description); + + if (i % 2 != 0) + Console.WriteLine (); + } + + Fill (); + RefreshCurrentItem (selected: true); + + Console.CursorVisible = false; + string command = DoMenu (out val); + + Console.CursorVisible = true; + return command; + } + + // Display list's items starting from first element, which is currently visible + void Fill () + { + if (_lines.Count <= 0) + return; + + int count = 0; + int x = _lines [_viewIndex].PosX; + int y = _lines [_viewIndex].PosY; + + for (int i = _viewIndex; i < _lines.Count; ++i, ++y) + { + _lines [i].Display (); + if (++count >= _maxLineCount) + break; + } + + // All items fit in console's view + if (count >= _maxLineCount) + return; + + // Erase all items on console from last visible item until end of list + string text = new (' ', _maxWidth); + for (; count <= _maxLineCount - 1; ++count, ++y) + { + Console.SetCursorPosition (x, y); + Console.Write (text); + } + } + + + string DoMenu (out double val) + { + val = double.NaN; + + for (; ; ) + { + var key = Console.ReadKey (true).Key; + switch (key) + { + case ConsoleKey.UpArrow: + SelectPrevItem (); + break; + + case ConsoleKey.DownArrow: + SelectNextItem (); + break; + + case ConsoleKey.PageUp: + SelectFirstItem (); + break; + + case ConsoleKey.PageDown: + SelectLastItem (); + break; + + case ConsoleKey.B: // Back to calculation + Console.Clear (); + return "b"; + + case ConsoleKey.E: // Exit app + return "e"; + + case ConsoleKey.Enter: // Use result of previous calculation in new calculation + val = double.Parse (_log.Calculations [_selectedIndex].Result); + return "u"; + + case ConsoleKey.Delete: + Delete (); + if (_lines.Count <= 0) + return "r"; + + break; + + case ConsoleKey.R: // Delete all items in list + Reset (); + return "r"; + } + } + } + + void RefreshCurrentItem (bool selected = false) + { + if (_selectedIndex >= 0 && _selectedIndex < _lines.Count) + _lines [_selectedIndex].Display (selected); + } + + void SelectPrevItem () + { + if (_selectedIndex == 0) + return; + + // Deselect actual item + RefreshCurrentItem (); + + // If previous item is not more visible, move all items in list up one line + if (_lines [--_selectedIndex].PosY < _posYMin) + ScrollDown (); + + // Select current item + RefreshCurrentItem (selected: true); + } + + void SelectNextItem () + { + if (_selectedIndex == _lines.Count - 1) + return; + + // Deselect current item + RefreshCurrentItem (); + + // If mext item is not more visible, move all items in list down one line + if (_lines [++_selectedIndex].PosY >= _posYMax) + ScrollUp (); + + // Select current item + RefreshCurrentItem (selected: true); + } + + void SelectFirstItem () + { + // If there are more items in a list as a view can contain, then set the first item + // as a first visible item and then position all left items downd this item accordingly their order. + if (_lines.Count > _maxLineCount) + { + int y = _posYMin; + + foreach (var data in _lines) + data.PosY = y++; + } + + _selectedIndex = 0; + _viewIndex = 0; + + Fill (); + RefreshCurrentItem (selected: true); + } + + void SelectLastItem () + { + // If there are more items in a list as a view can contain, then set the last item + // as a last visible item and then position all left items above this item accordingly their order. + if (_lines.Count > _maxLineCount) + { + int y = _posYMax - 1; + for (int i = _lines.Count - 1; i >= 0; --i) + _lines [i].PosY = y--; + + _viewIndex = _lines.Count - _maxLineCount; + } + + _selectedIndex = _lines.Count - 1; + + Fill (); + RefreshCurrentItem (selected: true); + } + + void ScrollUp () + { + ++_viewIndex; + foreach (var data in _lines) + --data.PosY; + + Fill (); + } + + void ScrollDown () + { + --_viewIndex; + + foreach (var data in _lines) + ++data.PosY; + + Fill (); + } + + // Deletes selected item from list + void Delete () + { + // Move all items after deleted item up one line + for (int i = _selectedIndex + 1; i < _lines.Count; ++i) + --_lines [i].PosY; + + _lines.RemoveAt (_selectedIndex); + _log.Delete (_selectedIndex); + + // Last item in list was deleted? Select previous item. + if (_selectedIndex >= _lines.Count) + _selectedIndex = _lines.Count - 1; + + Fill (); + RefreshCurrentItem (selected: true); + } + + void Reset () + { + _lines.Clear (); + _log.Reset (); + } +} diff --git a/Calculator/Program.cs b/Calculator/Program.cs new file mode 100644 index 00000000..fc0fe3b0 --- /dev/null +++ b/Calculator/Program.cs @@ -0,0 +1,10 @@ + +using ProgramLogic; + +StateMashine mashine = new (); + +for (; ; ) +{ + if (!mashine.ProcessCurrentState ()) + break; +} diff --git a/Calculator/README.md b/Calculator/README.md new file mode 100644 index 00000000..4a76d9aa --- /dev/null +++ b/Calculator/README.md @@ -0,0 +1 @@ +# Calculator \ No newline at end of file diff --git a/Calculator/StateMashine.cs b/Calculator/StateMashine.cs new file mode 100644 index 00000000..a0a8e877 --- /dev/null +++ b/Calculator/StateMashine.cs @@ -0,0 +1,413 @@ + +using CalculatorLibrary; +using System.Text.RegularExpressions; + +namespace ProgramLogic; + +// +// State-mashine for calculator. It has all logic to move the programm +// from one determined state to another +// + +public class StateMashine +{ + delegate void State (); + State? _nextState; + + // Position in console to show result of calculation + int _resPosX; + int _resPosY; + + // Position of current input from user + int _inputPosX; + int _inputPosY; + + string? _result; + + double _num1 = double.NaN; + double _num2 = double.NaN; + string _operation = string.Empty; + string? _resCalculation; + + Calculator _calculator = new (); + + public StateMashine () + { + _nextState = GetFirstOperand; + } + + // Return "false" means exit from program + public bool ProcessCurrentState () + { + if (_nextState == null) + return false; + + _nextState (); + return true; + } + + void GetFirstOperand () + { + _num1 = double.NaN; + _result = string.Empty; + + ShowTitle (); + ShowOperationsForGetOperand (); + + string op = GetUserInput (["v", "e"], out _num1); + if (!ProcessGetOperand (op)) + return; + + // Convert possible "-0" to "0" + if (Math.Abs (_num1) == 0) + _num1 = 0; + + FormatResult (); + + _nextState = GetOperation; + } + + void GetOperation () + { + _operation = string.Empty; + + ShowTitle (); + ShowOperations (); + FormatResult (); + + _operation = GetUserInput (["a", "s", "m", "d", "sqrt", "pow", "10", "e", "sin", "cos", "tg", "ctg"], out double val); + FormatResult (); + + if (IsOperationNeedSecondOperand ()) + _nextState = GetSecondOperand; + else + _nextState = DoCalculation; + } + + void GetSecondOperand () + { + _num2 = double.NaN; + ShowTitle (); + ShowOperationsForGetOperand (false); + + string op = GetUserInput (["v", "e"], out _num2); + if (!ProcessGetOperand (op, false)) + return; + + // Convert possible "-0" to "0" + if (Math.Abs (_num2) == 0) + _num2 = 0; + + FormatResult (); + + _nextState = DoCalculation; + } + + void DoCalculation () + { + _resCalculation = _calculator.DoOperation (_num1, _num2, _operation); + if (string.IsNullOrEmpty (_resCalculation)) + { + Gui.Notify ("This operation will result in a mathematical error."); + + if (IsOperationNeedSecondOperand ()) + _nextState = GetSecondOperand; + else + _nextState = GetOperation; + + return; + } + + _calculator.CalcLog.AddInfo (_result!, _resCalculation); + + FormatResult (); + + _nextState = AskToContinue; + } + + void AskToContinue () + { + ShowTitle (); + + Console.CursorVisible = false; + Console.WriteLine ("\nPress 'e' to exit the application, or press any other key to continue."); + if (Console.ReadKey (true).Key == ConsoleKey.E) + _nextState = null; + else + { + Console.CursorVisible = true; + _nextState = GetFirstOperand; + + _num1 = double.NaN; + _num2 = double.NaN; + _operation = string.Empty; + _resCalculation = string.Empty; + } + } + + void ShowTitle (bool showResult = true) + { + Console.SetCursorPosition (0, 0); + Console.Clear (); + + Gui.WriteColorText ("\n Calculator\n", ConsoleColor.Blue); + + string text = $" (launched {_calculator.CalcLog.UsedCount - 1} times)"; + Console.WriteLine (text); + + string line = new ('─', text.Length - 2); + Console.WriteLine ($" {line}\n"); + + (_resPosX, _resPosY) = Console.GetCursorPosition (); + + if (showResult) + { + Gui.WriteColorText ("Result: ", ConsoleColor.Green); + Console.WriteLine (_result); + Console.WriteLine (); + } + } + + void ShowOperationsForGetOperand (bool isFirstOperand = true) + { + string text = isFirstOperand ? "first" : "second"; + Console.WriteLine ($"Choose an operator from the following list or type {text} number to start calculation:"); + + Gui.WriteColorText ("\tv", ConsoleColor.Cyan); + Console.WriteLine (" - view results of previous calculations"); + + Gui.WriteColorText ("\te", ConsoleColor.Cyan); + Console.WriteLine (" - exit program"); + + Gui.WriteColorText (">> ", ConsoleColor.Yellow); + (_inputPosX, _inputPosY) = Console.GetCursorPosition (); + } + + void ShowOperations () + { + Tuple [] operations = + [ + Tuple.Create ("\ta", " - Add"), + Tuple.Create (" sqrt", " - Square root"), + Tuple.Create (" sin", " - Sine"), + Tuple.Create ("\ts", " - Subtract"), + Tuple.Create (" pow", " - x^y"), + Tuple.Create (" cos", " - Cosine"), + Tuple.Create ("\tm", " - Multiply"), + Tuple.Create (" 10", " - 10^x"), + Tuple.Create (" tg", " - Tangent"), + Tuple.Create ("\td", " - Divide"), + Tuple.Create (" e", " - e^x"), + Tuple.Create (" ctg", " - Cotangent") + ]; + + Console.WriteLine ("Choose an operator from the following list:"); + for (int i = 0; i < operations.Length; ++i) + { + var (operation, description) = operations [i]; + Gui.WriteColorText (operation, ConsoleColor.Cyan); + Console.Write (description); + + if ((i + 1) % 3 == 0) + Console.WriteLine (); + } + + Gui.WriteColorText (">> ", ConsoleColor.Yellow); + (_inputPosX, _inputPosY) = Console.GetCursorPosition (); + } + + string GetUserInput (string [] operations, out double number) + { + for (; ; ) + { + number = double.NaN; + if (operations == null || operations.Length <= 0) + return string.Empty; + + string? userInput = Console.ReadLine (); + if (string.IsNullOrEmpty (userInput)) + return string.Empty; + + // Clear inputed data on console (we have it in "userInput" parameter and won't see anymore) + var (x, y) = Console.GetCursorPosition (); + Console.SetCursorPosition (_inputPosX, _inputPosY); + Console.Write (new string (' ', userInput.Length)); + Console.SetCursorPosition (x, y); + + userInput = userInput.Trim ().ToLower (); + string regex = string.Join ("|", operations); + + // Regex.Match can throw exeptions for instance by input of symbols of unsupported codepage, + // therefore keep it i ntry / catch + try + { + var match = Regex.Match ($"[{regex}]", userInput); + if (!string.IsNullOrEmpty (match.Value)) + return match.Value; + + number = double.Parse (userInput); + } + catch + { + Gui.Notify (" Error: Unrecognized input. "); + Console.SetCursorPosition (_inputPosX, _inputPosY); + + continue; + } + + break; + } + + return string.Empty; + } + + // Process commands "e" (exit app) and "v" (view list of previous calculations) + bool ProcessGetOperand (string op, bool isFirstOperand = true) + { + // Exit program? + if (op == "e") + { + _nextState = null; + return false; + } + + // View list of previous calculations? + if (op == "v") + { + ShowTitle (false); + + LogList list = new (_calculator.CalcLog); + + double val = 0; + string command = list.Show (out val); + + switch (command) + { + case "b": // Back to calculations (return "false" means "don't change actual program's state) + return false; + + case "e": // Exit app + _nextState = null; + return false; + + case "u": // Use result of previous calculation + _result += string.Format ("{0:0.######}", val); + if (isFirstOperand) + _num1 = val; + else + _num2 = val; + + UpdateResult (); + break; + + default: + return false; + } + } + + return true; + } + + // Creates full string of actual calculation and display it to console + void FormatResult () + { + // Clear pervious calculation on console + if (_result?.Length > 0) + { + _result = new string (' ', _result.Length); + UpdateResult (); + } + + // Append first operand + _result = string.Format ("{0:0.######}", _num1); + + // Append operation + switch (_operation) + { + case "a": + _result += " + "; + break; + + case "s": + _result += " - "; + break; + + case "m": + _result += " x "; + break; + + case "d": + _result += " / "; + break; + + case "sqrt": + _result = $"sqrt ({_result})"; + break; + + case "pow": + _result += " ^ "; + break; + + case "10": + if (_num1 < 0) + _result = $"10 ^ ({_result})"; + else + _result = $"10 ^ {_result}"; + break; + + case "e": + if (_num1 < 0) + _result = $"e ^ ({_result})"; + else + _result = $"e ^ {_result}"; + break; + + case "sin": + _result = $"sin ({_result}°)"; + break; + + case "cos": + _result = $"cos ({_result}°)"; + break; + + case "tg": + _result = $"tg ({_result}°)"; + break; + + case "ctg": + _result = $"ctg ({_result}°)"; + break; + } + + // Append second operand if present + if (!double.IsNaN (_num2)) + { + if (_num2 < 0) + _result += string.Format ("({0:0.######})", _num2); + else + _result += string.Format ("{0:0.######}", _num2); + } + + // Append result of calculation + if (!string.IsNullOrEmpty (_resCalculation)) + _result += $" = {_resCalculation}"; + + UpdateResult (); + } + + void UpdateResult () + { + var (x, y) = Console.GetCursorPosition (); + Console.SetCursorPosition (_resPosX, _resPosY); + + Gui.WriteColorText ("Result: ", ConsoleColor.Green); + Console.Write (_result); + + Console.SetCursorPosition (x, y); + } + + bool IsOperationNeedSecondOperand () + { + return _operation == "a" || _operation == "s" || _operation == "m" || _operation == "d" || _operation == "pow"; + } +}