Skip to content

Commit

Permalink
Add DigiTally for counting votes
Browse files Browse the repository at this point in the history
  • Loading branch information
ermshiperete committed Apr 5, 2024
1 parent 23f78fd commit 4c67fe4
Show file tree
Hide file tree
Showing 16 changed files with 1,577 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added

- DigiTally, a tool to count the ballots

### Fixed

- replaced non-ASCII characters in encrypted filename (#41)
Expand Down
20 changes: 20 additions & 0 deletions DigiTally/DigiTally.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<RootNamespace>DigitaleBriefwahl.Tally</RootNamespace>
<AssemblyName>DigiTally</AssemblyName>
<OutputType>Exe</OutputType>
<OutputPath>../output/$(Configuration)/DigiTally</OutputPath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="GitVersion.MsBuild" Version="5.12.0" PrivateAssets="true" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DigitaleBriefwahl.ExceptionHandling\DigitaleBriefwahl.ExceptionHandling.csproj" />
<ProjectReference Include="..\DigitaleBriefwahl\DigitaleBriefwahl.csproj" />
</ItemGroup>

</Project>
58 changes: 58 additions & 0 deletions DigiTally/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) 2024 Eberhard Beilharz
// This software is licensed under the GNU General Public License version 3
// (https://opensource.org/licenses/GPL-3.0)

using System;
using System.IO;
using System.Text;
using DigitaleBriefwahl.ExceptionHandling;
using DigitaleBriefwahl.Model;

namespace DigitaleBriefwahl.Tally
{
public class Program
{
public static string TallyBallots(string configName, string ballotsDirectory)
{
var bldr = new StringBuilder();
var configuration = Configuration.Configure(configName);
var readBallot = new ReadBallots(configName);

foreach (var filename in Directory.GetFiles(ballotsDirectory))
{
readBallot.AddBallot(filename);
}

bldr.AppendLine(configuration.Title);
bldr.AppendLine(new string('=', configuration.Title.Length));
bldr.AppendLine();
bldr.AppendLine(
$"Total of {readBallot.NumberOfBallots} ballots, thereof {readBallot.NumberOfInvalidBallots} at least partially invalid.");
bldr.AppendLine();
bldr.Append(readBallot.GetResultString());
return bldr.ToString();
}
public static void Main(string[] args)
{
ExceptionLogging.Initialize("5012aef9a281f091c1fceea40c03003b", "DigiTally", args);

// Setup command line options
if (args.Length <= 0 || args[0] == "--help")
{
Console.WriteLine("Usage:");
Console.WriteLine("DigiTally.exe ballotsDirectory [configFile]");
Console.WriteLine("ballotsDirectory: directory that contains the decrypted ballots");
Console.WriteLine("configFile: optional name and path of the config file");
return;
}
var configName = args.Length > 1 ? args[1] : Configuration.ConfigName;
if (!File.Exists(configName))
{
Console.WriteLine($"Can't find {configName}. Exiting.");
return;
}

Console.WriteLine(TallyBallots(configName, args[0]));
}
}
}
84 changes: 84 additions & 0 deletions DigiTally/ReadBallots.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) 2024 Eberhard Beilharz
// This software is licensed under the GNU General Public License version 3
// (https://opensource.org/licenses/GPL-3.0)

using System.Collections.Generic;
using System.IO;
using System.Text;
using DigitaleBriefwahl.Model;

namespace DigitaleBriefwahl.Tally
{
public class ReadBallots
{
private Configuration _configuration;
private Dictionary<ElectionModel, Dictionary<string, CandidateResult>> _results;

public ReadBallots(string configFileName)
{
_results = new Dictionary<ElectionModel, Dictionary<string, CandidateResult>>();
_configuration = Configuration.Configure(configFileName);
}

public bool AddBallot(string ballotFileName)
{
using var ballotFile = new StreamReader(ballotFileName);
var title = ballotFile.ReadLine(); // The election
var separator = ballotFile.ReadLine(); // ============
if (string.IsNullOrWhiteSpace(separator) || !separator.StartsWith("="))
{
return false;
}
SkipEmptyLines(ballotFile);
var ballotIsPartiallyInvalid = false;
foreach (var election in _configuration.Elections)
{
var electionTitle = ballotFile.ReadLine(); // Election
separator = ballotFile.ReadLine(); // --------
if (string.IsNullOrWhiteSpace(separator) || !separator.StartsWith("--") || electionTitle != election.Name)
{
return false;
}

var prevInvalid = election.Invalid;
_results[election] = election.ReadVotesFromBallot(ballotFile, _results.TryGetValue(election, out var result) ? result: null);
ballotIsPartiallyInvalid |= election.Invalid > prevInvalid;
}

NumberOfBallots++;

if (ballotIsPartiallyInvalid)
NumberOfInvalidBallots++;

return true;
}

private void SkipEmptyLines(StreamReader stream)
{
int peek;
while ((peek = stream.Peek()) > -1)
{
if ((char)peek != '\r' && (char)peek != '\n')
return;

stream.ReadLine();
}
}

public int NumberOfBallots { get; private set; }
public int NumberOfInvalidBallots { get; private set; }
public Dictionary<ElectionModel, Dictionary<string, CandidateResult>> Results => _results;

public string GetResultString()
{
var strBuilder = new StringBuilder();
foreach (var election in _results)
{
strBuilder.AppendLine($"{election.Key.Name}");
strBuilder.AppendLine(new string('-', election.Key.Name.Length));
strBuilder.AppendLine(election.Key.GetResultString(election.Value));
}
return strBuilder.ToString();
}
}
}
157 changes: 157 additions & 0 deletions DigiTallyTests/AcceptanceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright (c) 2024 Eberhard Beilharz
// This software is licensed under the GNU General Public License version 3
// (https://opensource.org/licenses/GPL-3.0)

using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using NUnit.Framework;

namespace DigitaleBriefwahl.Tally.Tests
{
[TestFixture]
public class AcceptanceTests
{
private string _configFileName;
private string _ballotDirectoryName;

[SetUp]
public void Setup()
{
_ballotDirectoryName = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(_ballotDirectoryName);
_configFileName = Path.GetTempFileName();
File.WriteAllText(_configFileName, @"[Wahlen]
Titel=The election
Wahl1=Election1
Wahl2=Election2
Email=election@example.com
PublicKey=12345678.asc
[Election1]
Text=Some description
Typ=Weighted
Stimmen=2
Kandidat1=Mickey Mouse
Kandidat2=Donald Duck
Kandidat3=Dagobert Duck
Kandidat4=Daisy Duck
[Election2]
Text=Some description
Typ=YesNo
Stimmen=2
Kandidat1=One
Kandidat2=Two
Kandidat3=Three
Kandidat4=Four
");
}

[TearDown]
public void Teardown()
{
File.Delete(_configFileName);
Directory.Delete(_ballotDirectoryName, true);
}

[Test]
public void EndToEnd()
{
var ballotFileName1 = Path.Combine(_ballotDirectoryName, Path.GetRandomFileName());
File.WriteAllText(ballotFileName1, "The election\r\n" +
"============\r\n" +
"\r\n" +
"Election1\r\n" +
"--------\r\n" +
"(2 Stimmen; Wahl der Reihenfolge nach mit 1.-2. kennzeichnen)\r\n" +
"2. Mickey Mouse\r\n" +
" Donald Duck\r\n" +
"1. Dagobert Duck\r\n" +
" Daisy Duck\r\n" +
"\r\n" +
"Election2\r\n" +
"--------\r\n" +
"(J=Ja, E=Enthaltung, N=Nein)\r\n" +
"1. [J] One\r\n" +
"2. [N] Two\r\n" +
"3. [J] Three\r\n" +
"4. [E] Four\r\n" +
"\r\n" +
"\r\n" +
"12345\r\n");

var ballotFileName2 = Path.Combine(_ballotDirectoryName, Path.GetRandomFileName());
File.WriteAllText(ballotFileName2, "The election\r\n" +
"============\r\n" +
"\r\n" +
"Election1\r\n" +
"--------\r\n" +
"(2 Stimmen; Wahl der Reihenfolge nach mit 1.-2. kennzeichnen)\r\n" +
" Mickey Mouse\r\n" +
"1. Donald Duck\r\n" +
"2. Dagobert Duck\r\n" +
" Daisy Duck\r\n" +
"\r\n" +
"Election2\r\n" +
"--------\r\n" +
"(J=Ja, E=Enthaltung, N=Nein)\r\n" +
"1. [N] One\r\n" +
"2. [J] Two\r\n" +
"3. [E] Three\r\n" +
"4. [E] Four\r\n" +
"\r\n" +
"\r\n" +
"12345\r\n");

var ballotFileName3 = Path.Combine(_ballotDirectoryName, Path.GetRandomFileName());
File.WriteAllText(ballotFileName3, "The election\r\n" +
"============\r\n" +
"\r\n" +
"Election1\r\n" +
"--------\r\n" +
"(2 Stimmen; Wahl der Reihenfolge nach mit 1.-2. kennzeichnen)\r\n" +
"1. Mickey Mouse\r\n" +
" Donald Duck\r\n" +
"2. Dagobert Duck\r\n" +
" Daisy Duck\r\n" +
"\r\n" +
"Election2\r\n" +
"--------\r\n" +
"(J=Ja, E=Enthaltung, N=Nein)\r\n" +
"1. [X] One\r\n" +
"2. [E] Two\r\n" +
"3. [N] Three\r\n" +
"4. [E] Four\r\n" +
"\r\n" +
"\r\n" +
"12345\r\n");

var output = Program.TallyBallots(_configFileName, _ballotDirectoryName);

Assert.That(output, Is.EqualTo(@"The election
============
Total of 3 ballots, thereof 1 at least partially invalid.
Election1
---------
1. Dagobert Duck (4 points)
2. Mickey Mouse (3 points)
Donald Duck (2 points)
Daisy Duck (0 points)
(3 ballots, thereof 0 invalid)
Election2
---------
One: 1 J, 1 N, 0 E
Two: 1 J, 1 N, 0 E
Three: 1 J, 0 N, 1 E
Four: 0 J, 0 N, 2 E
(3 ballots, thereof 1 invalid)
"));
}

}
}
18 changes: 18 additions & 0 deletions DigiTallyTests/DigiTallyTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>DigitaleBriefwahl.Tally.Tests</RootNamespace>
<AssemblyTitle>DigiTallyTests</AssemblyTitle>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<OutputPath>bin/$(Configuration)</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ini-parser" Version="2.5.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DigiTally\DigiTally.csproj" />
</ItemGroup>
</Project>
Loading

0 comments on commit 4c67fe4

Please sign in to comment.