Initial commit

This commit is contained in:
github-classroom[bot] 2025-06-03 15:18:01 +00:00 committed by GitHub
commit 4b1294b32d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 5134 additions and 0 deletions

3551
.editorconfig Normal file

File diff suppressed because it is too large Load diff

583
.gitignore vendored Normal file
View file

@ -0,0 +1,583 @@
# Created by https://www.toptal.com/developers/gitignore/api/csharp,visualstudio
# Edit at https://www.toptal.com/developers/gitignore?templates=csharp,visualstudio
### Csharp ###
## 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/main/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/
[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
*.tlog
*.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 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
# 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/
# Visual Studio History (VSHistory) files
.vshistory/
# 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
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
### VisualStudio ###
# User-specific files
# User-specific files (MonoDevelop/Xamarin Studio)
# Mono auto generated files
# Build results
# Visual Studio 2015/2017 cache/options directory
# Uncomment if you have tasks that create the project's static files in wwwroot
# Visual Studio 2017 auto generated files
# MSTest test Results
# NUnit
# Build Results of an ATL Project
# Benchmark Results
# .NET Core
# ASP.NET Scaffolding
# StyleCop
# Files built by Visual Studio
# Chutzpah Test files
# Visual C++ cache files
# Visual Studio profiler
# Visual Studio Trace Files
# TFS 2012 Local Workspace
# Guidance Automation Toolkit
# ReSharper is a .NET coding add-in
# TeamCity is a build add-in
# DotCover is a Code Coverage Tool
# AxoCover is a Code Coverage Tool
# Coverlet is a free, cross platform Code Coverage Tool
# Visual Studio code coverage results
# NCrunch
# MightyMoose
# Web workbench (sass)
# Installshield output folder
# DocProject is a documentation generator add-in
# Click-Once directory
# Publish Web Output
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
# 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
# NuGet Packages
# NuGet Symbol Packages
# The packages folder can be ignored because of Package Restore
# except build/, which is used as an MSBuild target.
# Uncomment if necessary however generally it will be regenerated when needed
# NuGet v3's project.json files produces more ignorable files
# Microsoft Azure Build Output
# Microsoft Azure Emulator
# Windows Store app package directories and files
# Visual Studio cache files
# files ending in .cache can be ignored
# but keep track of directories ending in .cache
# Others
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
# RIA/Silverlight projects
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
# SQL Server files
# Business Intelligence projects
# Microsoft Fakes
# GhostDoc plugin setting file
# Node.js Tools for Visual Studio
# Visual Studio 6 build log
# Visual Studio 6 workspace options file
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
# Visual Studio 6 technical files
# Visual Studio LightSwitch build output
# Paket dependency manager
# FAKE - F# Make
# CodeRush personal settings
# Python Tools for Visual Studio (PTVS)
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
# Telerik's JustMock configuration file
# BizTalk build output
# OpenCover UI analysis results
# Azure Stream Analytics local run output
# MSBuild Binary and Structured Log
# NVidia Nsight GPU debugger configuration file
# MFractors (Xamarin productivity tool) working folder
# Local History for Visual Studio
# Visual Studio History (VSHistory) files
# BeatPulse healthcheck temp database
# Backup folder for Package Reference Convert tool in Visual Studio 2017
# Ionide (cross platform F# VS Code tools) working folder
# Fody - auto-generated XML schema
# VS Code files for those working on multiple tools
# Local History for Visual Studio Code
# Windows Installer files from build outputs
# JetBrains Rider
### VisualStudio Patch ###
# Additional files built by Visual Studio
# End of https://www.toptal.com/developers/gitignore/api/csharp,visualstudio

View file

@ -0,0 +1,233 @@
using MathInterpreter.Core;
namespace MathInterpreter.Test;
public sealed class ExpressionScannerTests
{
[Theory]
[InlineData("3abc", 3, 1, "abc")]
[InlineData("35gh", 35, 2, "gh")]
[InlineData("3afc2", 3, 1, "afc2")]
[InlineData(" 3abc", 3, 1, "abc")]
[InlineData("2 3abc", 23, 2, "abc")]
[InlineData("2 3 abc", 23, 2, "abc")]
[InlineData("78944", 78944, 5, "")]
[InlineData("12?!", 12, 2, "?!")]
public void ScanNumber_Simple_Unsigned(string expression, int expectedValue, int expectedLength,
string expectedRemainingExpression)
{
ScanResult<int> result = ExpressionScanner.ScanNumber(expression.AsSpan(), false);
result.Value.Should().Be(expectedValue);
result.Length.Should().Be(expectedLength);
result.RemainingExpression.ToString().Should().Be(expectedRemainingExpression);
}
[Theory]
[InlineData("+3abc", 3, 1, "abc")]
[InlineData("+5ab3c", 5, 1, "ab3c")]
[InlineData("+356abc", 356, 3, "abc")]
[InlineData("-3abc", -3, 1, "abc")]
[InlineData("-8ab2c", -8, 1, "ab2c")]
[InlineData("-37abc", -37, 2, "abc")]
[InlineData(" - 3abc", -3, 1, "abc")]
[InlineData("+ 3abc", 3, 1, "abc")]
public void ScanNumber_Simple_Signed(string expression, int expectedValue, int expectedLength,
string expectedRemainingExpression)
{
ScanResult<int> result = ExpressionScanner.ScanNumber(expression.AsSpan(), true);
result.Value.Should().Be(expectedValue);
result.Length.Should().Be(expectedLength);
result.RemainingExpression.ToString().Should().Be(expectedRemainingExpression);
}
[Theory]
[InlineData("cba")]
[InlineData("c1b2a3")]
[InlineData(" t")]
[InlineData(" t2")]
public void ScanNumber_NoDigits(string invalidExpression)
{
var action = BuildScanNumberWrapper(invalidExpression, false);
action.Should().Throw<NumberFormatException>()
.WithMessage($"Unable to read number from beginning of '{invalidExpression}'");
}
[Theory]
[InlineData("+56")]
[InlineData("-90")]
public void ScanNumber_SignNotAllowed(string invalidExpression)
{
var action = BuildScanNumberWrapper(invalidExpression, false);
action.Should().Throw<NumberFormatException>()
.WithMessage($"Unable to read number from beginning of '{invalidExpression}' " +
"(Sign is not allowed here)");
}
[Theory]
[InlineData("++56")]
[InlineData("+-56")]
[InlineData("--90")]
[InlineData("-+90")]
public void ScanNumber_MultipleSignNotAllowed(string invalidExpression)
{
var action = BuildScanNumberWrapper(invalidExpression, true);
action.Should().Throw<NumberFormatException>()
.WithMessage($"Unable to read number from beginning of '{invalidExpression}' " +
"(Sign may occur only once)");
}
[Theory]
[InlineData("a+56")]
[InlineData("b-56")]
[InlineData("hello")]
[InlineData("-")]
[InlineData("+")]
[InlineData(" ")]
[InlineData(" a ")]
[InlineData(" a6 ")]
[InlineData("?1")]
[InlineData("")]
public void ScanNumber_Invalid(string invalidExpression)
{
var action = BuildScanNumberWrapper(invalidExpression, true);
action.Should().Throw<NumberFormatException>()
.WithMessage($"Unable to read number from beginning of '{invalidExpression}'");
}
[Theory]
[InlineData("+", Operator.Plus, "")]
[InlineData("-abc", Operator.Minus, "abc")]
[InlineData("* abc", Operator.Multiply, " abc")]
[InlineData("/abc ", Operator.Divide, "abc ")]
public void ScanOperator_Simple(string expression, Operator expectedOperator, string remainingExpression)
{
ScanResult<Operator> result = ExpressionScanner.ScanOperator(expression);
result.Value.Should().Be(expectedOperator);
result.Length.Should().Be(1);
result.RemainingExpression.ToString().Should().Be(remainingExpression);
}
[Theory]
[InlineData("&")]
[InlineData("abc")]
[InlineData(" abc")]
[InlineData("g /")]
public void ScanOperator_Invalid(string expression)
{
var action = BuildScanOperatorWrapper(expression);
action.Should().Throw<OperatorException>()
.WithMessage($"Unknown operator: {expression.TrimStart()[0]}");
}
[Fact]
public void ScanOperator_Empty()
{
var action = BuildScanOperatorWrapper(string.Empty);
action.Should().Throw<OperatorException>().WithMessage("Empty expression");
}
[Theory]
[InlineData("123abc", 123, 3, "abc")]
[InlineData("23.49cba", 23.49, 5, "cba")]
[InlineData("0.45ab2c", 0.45, 4, "ab2c")]
[InlineData("0.452ab1c", 0.452, 5, "ab1c")]
public void ScanOperand_Simple(string expression, double expectedValue, int expectedLength,
string remainingExpression)
{
ScanResult<double> result = ExpressionScanner.ScanOperand(expression);
result.Value.Should().BeApproximately(expectedValue, double.Epsilon);
result.Length.Should().Be(expectedLength);
result.RemainingExpression.ToString().Should().Be(remainingExpression);
}
[Theory]
[InlineData("+123", 123, 3, "")]
[InlineData("+123abc", 123, 3, "abc")]
[InlineData("+123.12abc", 123.12, 6, "abc")]
[InlineData("-123", -123, 3, "")]
[InlineData("-123cb", -123, 3, "cb")]
[InlineData("-123.89cb", -123.89, 6, "cb")]
public void ScanOperand_Simple_Signed(string expression, double expectedValue, int expectedLength,
string remainingExpression)
{
ScanResult<double> result = ExpressionScanner.ScanOperand(expression);
result.Value.Should().BeApproximately(expectedValue, double.Epsilon);
result.Length.Should().Be(expectedLength);
result.RemainingExpression.ToString().Should().Be(remainingExpression);
}
[Theory]
[InlineData("abc")]
[InlineData("")]
[InlineData(" ")]
[InlineData("cb43")]
public void ScanOperand_Invalid_NoNumber(string expression)
{
BuildScanOperandWrapper(expression).Should()
.Throw<ExpressionFormatException>()
.WithMessage("Operand is missing");
}
[Theory]
[InlineData(".12")]
[InlineData(" .12")]
[InlineData(".12.55")]
public void ScanOperand_Invalid_Integral(string expression)
{
BuildScanOperandWrapper(expression).Should()
.Throw<ExpressionFormatException>()
.WithMessage("Unable to parse integral part of number");
}
[Theory]
[InlineData("123.a")]
[InlineData("123. ")]
[InlineData("123. b")]
[InlineData("123.g5")]
[InlineData("123.-5")]
[InlineData("123.+5")]
public void ScanOperand_Invalid_Fractional(string expression)
{
BuildScanOperandWrapper(expression).Should()
.Throw<ExpressionFormatException>()
.WithMessage("Unable to parse fractional part of number");
}
private static Action BuildScanNumberWrapper(string expression, bool allowSign)
{
return () =>
{
ReadOnlySpan<char> span = expression.AsSpan();
ExpressionScanner.ScanNumber(span, allowSign);
};
}
private static Action BuildScanOperandWrapper(string expression)
{
return () =>
{
ReadOnlySpan<char> span = expression.AsSpan();
ExpressionScanner.ScanOperand(span);
};
}
private static Action BuildScanOperatorWrapper(string expression)
{
return () =>
{
ReadOnlySpan<char> span = expression.AsSpan();
ExpressionScanner.ScanOperator(span);
};
}
}

View file

@ -0,0 +1,63 @@
using MathInterpreter.Core;
namespace MathInterpreter.Test;
public sealed class MathExpressionTests
{
[Theory]
[InlineData("123 + 45")]
[InlineData("123+ 45")]
[InlineData("41 - 12")]
[InlineData("41 -12")]
[InlineData("12 * 0.9")]
[InlineData("12*0.9")]
[InlineData("2.32 / -3.9198")]
public void MathExpression_Construction_Valid(string expression)
{
var action = () =>
{
var _ = new MathExpression(expression);
};
action.Should().NotThrow("Valid expression passed, don't expect any exceptions");
}
[Theory]
[InlineData("123 + ", "Right operand is missing")]
[InlineData("+ 456", "Operator is missing")]
[InlineData("123 / 0", "Division by zero is not allowed")]
[InlineData("123 abc 456", "Unknown operator: a")]
public void MathExpression_Construction_Invalid(string expression, string expectedExceptionMessage)
{
var action = () =>
{
var _ = new MathExpression(expression);
};
action.Should().Throw<ExpressionException>().WithMessage(expectedExceptionMessage);
}
[Theory]
[InlineData("2 + 2", 4.0)]
[InlineData("10 - -3", 13.0)]
[InlineData("5.2 * 2", 10.4)]
[InlineData("-8 / 2", -4.0)]
public void Result_Valid(string expression, double expectedResult)
{
var mathExpression = new MathExpression(expression);
mathExpression.Result.Should().Be(expectedResult);
}
[Theory]
[InlineData("2 + 2aa", "2 + 2")]
[InlineData("10 - 3", "10 - 3")]
[InlineData("5 * 2f", "5 * 2")]
[InlineData(" 8 / 2", "8 / 2")]
public void StringRepresentation(string expression, string expectedString)
{
var mathExpression = new MathExpression(expression);
mathExpression.ToString().Should().Be(expectedString);
}
}

View file

@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Using Include="FluentAssertions" />
<Using Include="Xunit" />
<Using Include="NSubstitute" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AwesomeAssertions" Version="8.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MathInterpreter\MathInterpreter.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,32 @@
using MathInterpreter.Core;
namespace MathInterpreter.Test;
public sealed class ScanResultTests
{
[Fact]
public void ScanResult_Construction_Valid()
{
const int ExpectedValue = -123;
const int ExpectedLength = 3;
const string ExpectedRemainingExpression = "abc";
var result = new ScanResult<int>(ExpectedValue, ExpectedLength, ExpectedRemainingExpression);
result.Value.Should().Be(ExpectedValue);
result.Length.Should().Be(ExpectedLength);
result.RemainingExpression.ToString().Should().Be(ExpectedRemainingExpression);
}
[Fact]
public void ScanResult_Construction_Invalid()
{
var action = () =>
{
var _ = new ScanResult<int>(1, -2, string.Empty);
};
action.Should().Throw<ArgumentOutOfRangeException>()
.WithMessage("Length must be non-negative (Parameter 'length')");
}
}

22
MathInterpreter.sln Normal file
View file

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MathInterpreter", "MathInterpreter\MathInterpreter.csproj", "{E7084B85-BF73-4BD2-B980-734680DB48E4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MathInterpreter.Test", "MathInterpreter.Test\MathInterpreter.Test.csproj", "{8CFD9CF1-6D9C-4A7A-8FE5-2B5F0A6B2905}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E7084B85-BF73-4BD2-B980-734680DB48E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E7084B85-BF73-4BD2-B980-734680DB48E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7084B85-BF73-4BD2-B980-734680DB48E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7084B85-BF73-4BD2-B980-734680DB48E4}.Release|Any CPU.Build.0 = Release|Any CPU
{8CFD9CF1-6D9C-4A7A-8FE5-2B5F0A6B2905}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CFD9CF1-6D9C-4A7A-8FE5-2B5F0A6B2905}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CFD9CF1-6D9C-4A7A-8FE5-2B5F0A6B2905}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CFD9CF1-6D9C-4A7A-8FE5-2B5F0A6B2905}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,9 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MathInterpreter.App"
RequestedThemeVariant="Dark">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

View file

@ -0,0 +1,23 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace MathInterpreter;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}

View file

@ -0,0 +1,38 @@
namespace MathInterpreter.Core;
/// <summary>
/// Collection of constants used throughout the project
/// </summary>
public static class Const
{
/// <summary>
/// The plus (+) character used both as sign and operator
/// </summary>
public const char PlusSign = '+';
/// <summary>
/// The minus (-) character used both as sign and operator
/// </summary>
public const char MinusSign = '-';
/// <summary>
/// The times (*) character used as operator
/// </summary>
public const char TimesSign = '*';
/// <summary>
/// The division (/) character used as operator
/// </summary>
public const char DivSign = '/';
/// <summary>
/// The decimal separator (.) character used in numbers to separate the integral from the fractional part
/// </summary>
public const char DecimalSeparator = '.';
/// <summary>
/// An error message used when a part of the code is reached that should never be reached due to
/// previous, proper validation
/// </summary>
public const string ImpossibleErrorMessage = "If validation works as intended, this should never happen";
}

View file

@ -0,0 +1,5 @@
using System;
namespace MathInterpreter.Core;
// TODO implement all exception classes

View file

@ -0,0 +1,47 @@
using System;
namespace MathInterpreter.Core;
/// <summary>
/// Provides utility methods for scanning character streams for certain parts of a maths expression
/// </summary>
public static class ExpressionScanner
{
/// <summary>
/// Scans the given expression for an operator (+, -, *, /)
/// </summary>
/// <param name="expression">Expression to process</param>
/// <returns>A scan result containing the operator found and the remaining part of the expression</returns>
/// <exception cref="OperatorException">Thrown if none or an unknown operator is found</exception>
public static ScanResult<Operator> ScanOperator(ReadOnlySpan<char> expression)
{
// TODO
throw new NotImplementedException();
}
/// <summary>
/// Scans the given expression for an operand which is a number which have to have an integral
/// part and may have a fractional part. It may also have a sign (+, -) in front of it.
/// </summary>
/// <param name="expression">Expression to process</param>
/// <returns>A scan result containing the parsed number and the remaining part expression</returns>
/// <exception cref="ExpressionFormatException">Thrown if a problem with the format of the number is found</exception>
public static ScanResult<double> ScanOperand(ReadOnlySpan<char> expression)
{
// TODO
throw new NotImplementedException();
}
/// <summary>
/// Scans the given expression for a whole number which may have a sign (+, -) in front of it
/// </summary>
/// <param name="expression">Expression to process</param>
/// <param name="allowSign">Flag indicating if a pre-fix sign (+, -) is allowed or not</param>
/// <returns>A scan result containing the parsed number and the remaining part expression</returns>
/// <exception cref="NumberFormatException">Thrown if no or an invalid number is found</exception>
public static ScanResult<int> ScanNumber(ReadOnlySpan<char> expression, bool allowSign)
{
// TODO
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,67 @@
using System;
using System.Diagnostics;
namespace MathInterpreter.Core;
/// <summary>
/// Represents a mathematical expression
/// </summary>
public sealed class MathExpression
{
// TODO
/*
private double? _leftOperand;
private Operator? _operator;
private double? _result;
private double? _rightOperand;
*/
/// <summary>
/// Creates a new instance of <see cref="MathExpression" /> from the provided expression string.
/// Validates the provided expression and throws an exception if it is invalid.
/// </summary>
/// <param name="expressionString">The expression as a string</param>
/// <exception cref="OperatorException">Thrown if a problem with the operator is found</exception>
/// <exception cref="ExpressionFormatException">Thrown if a problem with the expression format is found</exception>
/// <exception cref="NumberValueException">Thrown if a problem with the value of a number is found</exception>
public MathExpression(string expressionString)
{
ValidateAndParse(expressionString.Trim().AsSpan());
}
/// <summary>
/// Gets the calculated result of the expression
/// </summary>
/// <exception cref="UnreachableException">
/// Thrown if validation during construction did not guard against an unexpected, invalid expression
/// </exception>
public double Result
{
get
{
// TODO
throw new NotImplementedException();
}
}
/// <summary>
/// Returns a string representation of the expression - this is not the originally provided string
/// but the cleaned up version
/// </summary>
/// <returns>String representation of the expression</returns>
/// <exception cref="UnreachableException">
/// Thrown if validation during construction did not guard against an unexpected, invalid expression
/// and left this instance in an invalid state
/// </exception>
public override string ToString()
{
// TODO
throw new NotImplementedException();
}
private void ValidateAndParse(ReadOnlySpan<char> expression)
{
// TODO
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,12 @@
namespace MathInterpreter.Core;
/// <summary>
/// Possible operators in an expression
/// </summary>
public enum Operator
{
Plus,
Minus,
Multiply,
Divide
}

View file

@ -0,0 +1,40 @@
using System;
namespace MathInterpreter.Core;
/// <summary>
/// Result object of scan operations
/// </summary>
/// <typeparam name="T">Type of the parsed value</typeparam>
public readonly ref struct ScanResult<T> where T : struct
{
/// <summary>
/// Creates a new scan result based on a successfully parsed value - if parsing failed an Exception
/// would have been thrown and this constructor would not be called
/// </summary>
/// <param name="value">Parsed value</param>
/// <param name="length">Number of characters (digits) of the parsed value</param>
/// <param name="remainingExpression">Remaining part of the expression after extracting and parsing the value</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown if an invalid argument is provided</exception>
public ScanResult(T value, int length, ReadOnlySpan<char> remainingExpression)
{
Value = value;
Length = -1; // TODO
RemainingExpression = remainingExpression;
}
/// <summary>
/// Gets the parsed value
/// </summary>
public T Value { get; }
/// <summary>
/// Gets the number of characters (digits) of the parsed value
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the remaining part of the expression after extracting and parsing the value
/// </summary>
public ReadOnlySpan<char> RemainingExpression { get; }
}

View file

@ -0,0 +1,24 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MathInterpreter"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="250"
x:Class="MathInterpreter.MainWindow"
x:DataType="local:MainWindow"
Width="400" Height="250"
WindowStartupLocation="CenterScreen"
Title="MathInterpreter">
<StackPanel Orientation="Vertical" Margin="20, 20" HorizontalAlignment="Stretch">
<TextBlock FontSize="34" HorizontalAlignment="Center">Math Interpreter</TextBlock>
<StackPanel Orientation="Horizontal" Margin="0,20,0,0" Spacing="8" HorizontalAlignment="Center">
<TextBox Width="160" HorizontalContentAlignment="Center" Watermark="Enter expression"
Name="ExpressionInput" VerticalContentAlignment="Center"></TextBox>
<Button FontSize="20" Width="40" HorizontalContentAlignment="Center"
VerticalContentAlignment="Center" Click="Calculate">=</Button>
<Border Width="100">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Name="Result">?</TextBlock>
</Border>
</StackPanel>
</StackPanel>
</Window>

View file

@ -0,0 +1,54 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using MathInterpreter.Core;
using MsBox.Avalonia;
using MsBox.Avalonia.Base;
using MsBox.Avalonia.Enums;
namespace MathInterpreter;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private async void Calculate(object sender, RoutedEventArgs e)
{
try
{
string input = ExpressionInput.Text ?? throw new InvalidOperationException("Input must not be empty");
var expression = new MathExpression(input);
Result.Text = expression.Result.ToString(CultureInfo.InvariantCulture);
}
catch (Exception ex)
{
string exStr = UnwrapException(ex);
await ShowError(exStr);
}
}
private static string UnwrapException(Exception exception)
{
string message = exception.Message;
if (exception.InnerException != null)
{
message += $"{Environment.NewLine}Inner exception: {UnwrapException(exception.InnerException)}";
}
return message;
}
private static async ValueTask ShowError(string message)
{
IMsBox<ButtonResult> box = MessageBoxManager.GetMessageBoxStandard("Error", message);
await box.ShowAsync();
}
}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifiers>win-x64;linux-x64;linux-arm64;osx-x64</RuntimeIdentifiers>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<PublishSingleFile>true</PublishSingleFile>
<PublishTrimmed>true</PublishTrimmed>
<AssemblyName>MathInterpreter</AssemblyName>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.8" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.8" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.8" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.8" />
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.8" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,26 @@
using System;
using Avalonia;
namespace MathInterpreter;
#region Nothing to see here :)
public class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) =>
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
private static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
#endregion

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="AvaloniaTest.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

BIN
pics/sample_run_error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
pics/sample_run_success.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

225
readme.adoc Normal file
View file

@ -0,0 +1,225 @@
:sectnums:
:nofooter:
:toc: left
:icons: font
:data-uri:
:source-highlighter: highlightjs
:stem:
= Exc.01 -- Math Interpreter
== Intro
This time you are going to implement an application which is able to parse and evaluate a mathematical expression entered by the user.
Sample Expressions:
* stem:[2 + 3]
* stem:[45.02 - 12.98]
* stem:[0.01 * 6]
* stem:[78 / 11.4]
* stem:[+9 * -2]
Since parsing a string like that is such a basic, first-grade task we are going to spice it up a little bit by at least providing a simple GUI.
Don't worry, all the work has been done for you this time, but you _can_ enjoy the (hopefully good) results of your implementation in a real GUI application -- with the exceptions you are producing being displayed in a message box.
See <<fig:sample,this screenshot>> for how it will look like.
=== Exceptions
We learnt that in most cases we want to throw common framework exceptions like `ArgumentException` or `InvalidOperationException` to indicate that something went wrong.
However, in this exercise we are going to create our own exceptions to indicate that something went wrong during parsing or evaluation of an expression.
This will allow you to learn how to create and work with your own exceptions.
IMPORTANT: You have to implement <<impl:exceptions,those classes>> before doing anything else, or the application can't compile!
=== Bonus Level: Experts Only
NOTE: *All non-experts*: if you do things correctly, you can treat `ReadOnlySpan<char>` just like a `string` and don't have to worry about it any further in this assignment.
For the experts among you, who are starting to become bored by the slow pace we are going at I have included two interesting things they can try out and learn about.
==== What is a `Span`
* A string is immutable, so every time you do something like that a new string is created
* In this exercise we are doing a lot of string mangling.
This can lead to a lot of (temporary) strings being created and destroyed.
For example substrings, trimmed strings, etc.
* However, all those strings are basically just views on the original string
* We can _optimize_ this scenario by using a `Span` (or rather `ReadOnlySpan`)
* A `Span` is a view on a memory region (can be a string, could be _any_ array or 'continuous block of bytes')
* Go ahead and https://learn.microsoft.com/en-us/dotnet/api/system.span-1?view=net-9.0[read the documentation], it's fascinating 😁
==== Publishing a GUI application
* Consider you want to create an application which can be downloaded or otherwise shipped to users
* Do you think sending them a zip archive containing the executable and all the (hundreds of) DLLs is a good idea?
* Can we assume that every user has the .NET runtime installed?
* Try to _publish_ the application of this assignment:
.. Run this command: +
`dotnet publish -c Release -r <RID> --self-contained true`
*** *Replace <RID> with*:
**** `win-x64` for a 64-bit Windows machine
**** `linux-x64` / `linux-arm64` for a 64-bit Linux machine
**** `macos-x64` / `macos-arm64` for a Mäci
*** Using `Release` configuration (you surely remember the session we did on differences between release and debug builds)
*** Publishing for a 64-bit machine -- when publishing self-contained the target platform (bitness, CPU architecture & OS) has to be specified, because native binaries are included
**** Note: we _cannot cross-compile_, so a Linux machine cannot create a _native_ Windows executable and vice versa
*** Providing a self-contained application which does _not_ require .NET to be installed on the target machine at all, we bring all the parts we need
.. Navigate to `bin\Release\<NET>\<RID>\publish`
*** Replace `<NET>` with your current platform
*** Replace `<RID>` with the target architecture
.. You find a `MathInterpreter` there which is a _standalone_ *executable* of the application that can be copied/shipped around
*** Ignore the `pdb` file, it's just for debugging
* That is quite nice! 🥳
* But you may think 'why is this file so huge (~32MB)'?
** Remember that it is containing not only your code, but also the GUI framework and the .NET framework and the .NET runtime 😮
** In fact, _unused_ parts of the framework have already been _trimmed_ (= removed), otherwise it would be 120MB or even bigger 🤯
** There are further optimizations like AOT compiling or having the runtime installed on the machine which can drastically reduce the size of the application, but that is a topic for another year 😉
== Implementation
TIP: The `Const` class contains useful constants you will need in your implementation
[[impl:exceptions]]
=== Exceptions
As stated before you'll create your own exceptions for this exercise.
[plantuml]
----
@startuml
hide empty members
class Exception {}
abstract class ExpressionException {
#ExpressionException(string, Exception? = null)
}
class ExpressionFormatException <<sealed>> {
+ExpressionFormatException(string, Exception? = null)
}
class OperatorException <<sealed>> {
+OperatorException(string)
}
class NumberFormatException <<sealed>> {
+NumberFormatException(ReadOnlySpan<char>, string? = null)
{static} -string FormatMessage(ReadOnlySpan<char>, string? = null)
}
class NumberValueException <<sealed>> {
+NumberValueException(string)
}
Exception <|-- ExpressionException
ExpressionException <|-- ExpressionFormatException
ExpressionException <|-- OperatorException
ExpressionException <|-- NumberValueException
ExpressionException <|-- NumberFormatException
@enduml
----
* You need to implement these classes first to allow the application to compile
* *Also add the full XMLDoc for these classes!*
* Hints:
** You'll have to call the `base` constructor frequently
** Mind the _optional_ parameters (e.g. `= null`)!
.`FormatMessage`
[%collapsible]
====
[source,csharp]
----
private static string FormatMessage(ReadOnlySpan<char> expression, string? reason = null)
{
reason = string.IsNullOrWhiteSpace(reason) ? string.Empty : $" ({reason})";
return $"Unable to read number from beginning of '{expression}'{reason}";
}
----
====
=== `ScanResult`
* This is a _struct_ encapsulating result of a scan operation.
* The implementation is mostly done, but in the constructor you have to check if the length is valid (>= 0) and throw a _proper_ `ArgumentOutOfRangeException` otherwise.
.Experts click here
[%collapsible]
====
* You have, of course, read about `Span`
* Now you can see that `ScanResult` is not _just_ a struct, but a `readonly ref struct`
* https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct[Read about `ref struct`]
* We _have_ to use this data structure here, because it _contains_ a `Span`, and a `Span` must not escape to the heap, so it may only be used as a field in data structures which will stay on the stack as well
** => you _can't_ have a `Span` as field of a class!
** This is a special type used only for special optimization scenarios, so it is obviously not general-purpose and harder to work with than a 'normal' type -- use only when really necessary and applicable!
====
=== `ExpressionScanner`
TIP: Remember to use https://www.geeksforgeeks.org/range-structure-in-c-sharp-8-0/[_ranges_] (e.g. "hello"[1..3] => "el")
* Provides several methods to _scan_ a string and extract a value
* `ScanNumber`
** Scans for a whole number
** A number may have a prefix of `+` or `-`, with the latter indicating a negative number
*** But if at all, than at most once!
** Whitespaces are skipped
** Once a character which is neither a digit nor a whitespace nor an allowed prefix is encountered, the scan is stopped
** Throw `NumberFormatException` if you encounter a problem
*** Expected messages: 'Sign is not allowed here', 'Sign may occur only once'
** Hint: `char.IsDigit`, `char.IsWhiteSpace` & `char.GetNumericValue` still exist
* `ScanOperand`
** Scans for an operand, which is a number
** While `ScanNumber` only readS whole numbers an operand may have a fractional part (e.g. `1.5`)
** Throw `ExpressionFormatException` if you encounter a problem
*** Expected messages: 'Unable to parse integral part of number', 'Operand is missing', 'Unable to parse fractional part of number'
** Hints:
*** Use `ScanNumber` twice
*** The integral part may contain a sign, but the fractional part does not
*** When converting the fractional part (e.g. `1234` => `0.1234`) you will need the number of digits -- that's what the `Length` property of `ScanResult` is for
*** When adding the fractional part you have to take into account of the integral part is negative or not
* `ScanOperator`
** Scans for an operator (`+`, `-`, `*`, `/`)
** Whitespaces are skipped
** Other characters are not allowed
** Throw `OperatorException` if you encounter a problem
*** Expected messages: 'Empty expression', 'Unknown operator: <OP>'
** Hints:
*** Length will always be 1 if parsing was successful
*** You will need an equivalent of `string.Empty` in your implementation and can use `ReadOnlySpan<char>.Empty` for that
NOTE: The difference between _scanning_ and _parsing_ in this scenario is that when you scan you digest a stream (of characters in our case) one by one until you extracted the desired value -- the _remainder_ of the stream can remain unaffected
=== `MathExpression`
* Represents a mathematical expression (e.g. `-2 * 3`)
* Upon instantiation the expression is parsed and validated, but not evaluated
** For validation use the methods of `ExpressionScanner`
* Calling `Result` will evaluate the expression and return the result
** This result is then _cached_ for subsequent calls!
* `ValidateAndParse`
** Parses the expression and validates it
** Parts are stored in the appropriate fields
** Exceptions:
*** Throw an `OperatorException` with message 'Operator is missing' if no operator is found
*** Throw an `ExpressionFormatException` with message 'Right operand is missing' if no second operand is found
*** Throw a `NumberValueException` with message 'Division by zero is not allowed' if operator is `/` and right operand is `0`
*** _Rethrow_ any other exception with type `ExpressionException`
*** If you encounter any _other_ exception _wrap_ (inner exception!) it into an `ExpressionFormatException` with message 'Unable to parse expression'
* `Result`
** On first call it calculates the result and stores it in the related field
** On subsequent calls it returns the cached result
** This method may (theoretically) throw an https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.unreachableexception?view=net-8.0[`UnreachableException`] which _strongly_ indicates that this section of code should never have been reached and the exception was only placed there to satisfy the compiler
*** If you implement the validation correctly it really won't ever been thrown 😉
* `ToString`
** Returns the _sanitized_ expression that will be used for evaluation
=== Sample Run
[[fig:sample]]
.Success
image::pics/sample_run_success.png[Sample Run - Success]
.Failure
image::pics/sample_run_error.png[Sample Run - Failure]