diff --git a/README.md b/README.md index fb9f12d..f7c5ff5 100644 --- a/README.md +++ b/README.md @@ -366,6 +366,7 @@ public class MyTestClass ``` + ### Comparers #### GameObjectNameComparer @@ -450,6 +451,7 @@ public class MyTestClass ``` + ### Constraints #### Destroyed @@ -479,6 +481,86 @@ public class MyTestClass > When used with operators, use it in method style. e.g., `Is.Not.Destroyed()` + +### Statistics APIs \[experimental\] + +`TestHelper.Statistics` namespace provides utilities for statistical testing, including assertions for pseudo-random number generators (PRNG) and statistical summary tools. + +> [!WARNING] +> This feature is experimental. + +> [!IMPORTANT] +> This feature is **NOT** statistical hypothesis testing tool. + +> [!NOTE] +> We plan to add probability distribution and various constraints in the future. + + +#### Experiment + +`Experiment` is a class for running experiments of PRNG. + +Usage: + +```csharp +[TestFixture] +public class MyStatisticalTest +{ + [Test] + public void Experiment_2D6() + { + var sampleSpace = Experiment.Run( + () => DiceGenerator.Roll(2, 6), // 2D6 + 1 << 20); // 1,048,576 times + + Assert.That(sampleSpace.Max, Is.EqualTo(12)); + Assert.That(sampleSpace.Min, Is.EqualTo(2)); + } +} +``` + + +#### Histogram + +`Histogram` is a class for plotting a histogram and calculating statistical summaries. + +Usage: + +```csharp +[TestFixture] +public class MyStatisticalTest +{ + [Test] + public void Histogram_2D6() + { + var sampleSpace = Experiment.Run( + () => DiceGenerator.Roll(2, 6), // 2D6 + 1 << 20); // 1,048,576 times + + var histogram = new Histogram(); + histogram.Plot(sampleSpace); + Debug.Log(histogram.GetSummary()); // Write to console + } +} +``` + +Console output example: + +``` +Experimental and Statistical Summary: + Sample size: 1,048,576 + Maximum: 12 + Minimum: 2 + Peak frequency: 174,554 + Valley frequency: 29,070 + Median: 87,490 + Mean: 95,325.09 + Histogram: ▁▂▃▅▆█▆▅▃▂▁ + (Each bar represents the frequency of values in equally spaced bins.) +``` + + + ### Runtime APIs `TestHelper.RuntimeInternals` assembly can be used from the runtime code because it does not depend on test-framework. @@ -570,17 +652,20 @@ public class MyTestClass > When loading the scene that is not in "Scenes in Build", use [BuildSceneAttribute](#BuildScene). + ### Editor Extensions #### Open Persistent Data Directory Select menu item **Window > Test Helper > Open Persistent Data Directory**, which opens the directory pointed to by [Application.persistentDataPath](https://docs.unity3d.com/ScriptReference/Application-persistentDataPath.html) in the Finder/ File Explorer. + #### Open Temporary Cache Directory Select menu item **Window > Test Helper > Open Temporary Cache Directory**, which opens the directory pointed to by [Application.temporaryCachePath](https://docs.unity3d.com/ScriptReference/Application-temporaryCachePath.html) in the Finder/ File Explorer. + ### JUnit XML format report If you specify path with `-testHelperJUnitResults` command line option, the test result will be written in JUnit XML format when the tests are finished. diff --git a/Runtime/Statistics.meta b/Runtime/Statistics.meta new file mode 100644 index 0000000..18e7b1b --- /dev/null +++ b/Runtime/Statistics.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 368a2ad122c14921ba5743ce1ca3f40f +timeCreated: 1736635545 \ No newline at end of file diff --git a/Runtime/Statistics/Experiment.cs b/Runtime/Statistics/Experiment.cs new file mode 100644 index 0000000..850f437 --- /dev/null +++ b/Runtime/Statistics/Experiment.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using System; + +namespace TestHelper.Statistics +{ + public static class Experiment + { + /// + /// Running experiments of pseudo-random number generator (PRNG). + /// + /// Method returns random value + /// Trail count + /// Type of random value + /// Sample space + public static SampleSpace Run(Func method, uint trailCount) + where T : IComparable + { + var sampleSpace = new SampleSpace(trailCount); + for (var i = 0; i < trailCount; i++) + { + sampleSpace.Add(method.Invoke()); + } + + return sampleSpace; + } + } +} diff --git a/Runtime/Statistics/Experiment.cs.meta b/Runtime/Statistics/Experiment.cs.meta new file mode 100644 index 0000000..3847628 --- /dev/null +++ b/Runtime/Statistics/Experiment.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ca4d572f43714582b98d2cc42787dcf5 +timeCreated: 1736615506 \ No newline at end of file diff --git a/Runtime/Statistics/Histograms.meta b/Runtime/Statistics/Histograms.meta new file mode 100644 index 0000000..34f9707 --- /dev/null +++ b/Runtime/Statistics/Histograms.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 695fe96068f14700a5d0e71b7a0de501 +timeCreated: 1736665430 \ No newline at end of file diff --git a/Runtime/Statistics/Histograms/Bin.cs b/Runtime/Statistics/Histograms/Bin.cs new file mode 100644 index 0000000..351da9d --- /dev/null +++ b/Runtime/Statistics/Histograms/Bin.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using System; + +namespace TestHelper.Statistics.Histograms +{ + /// + /// Bin of histogram. + /// + /// + public class Bin where T : IComparable + { + /// + /// Minimum value of this bin range. + /// + public T Min { get; private set; } + + /// + /// Maximum value (exclusive) of this bin range. + /// + public T Max { get; private set; } + + /// + /// Frequency of this bin. + /// + public uint Frequency { get; set; } + + /// + /// Label of this bin. + /// + public string Label + { + get + { + return Min.ToString(); + } + } + + /// + /// Constructor. + /// + /// Minimum value of this bin range + /// Maximum value (exclusive) of this bin range + public Bin(T minInclusive, T maxExclusive) + { + Min = minInclusive; + Max = maxExclusive; + Frequency = 0; + } + + /// + /// Constructor. + /// + /// Value of this bin range + public Bin(T value) : this(value, value) { } + + /// + /// Returns value is in this bin range. + /// + /// + /// true if value is in this bin range + public bool IsInRange(T value) + { + if (Min.CompareTo(Max) == 0) + { + return Min.CompareTo(value) == 0; + } + + return Min.CompareTo(value) <= 0 && Max.CompareTo(value) > 0; + } + } +} diff --git a/Runtime/Statistics/Histograms/Bin.cs.meta b/Runtime/Statistics/Histograms/Bin.cs.meta new file mode 100644 index 0000000..2a15e2c --- /dev/null +++ b/Runtime/Statistics/Histograms/Bin.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f1c827c08372480ea5ebca5e4427d1ca +timeCreated: 1736665452 \ No newline at end of file diff --git a/Runtime/Statistics/Histograms/Histogram.cs b/Runtime/Statistics/Histograms/Histogram.cs new file mode 100644 index 0000000..bb7dfe5 --- /dev/null +++ b/Runtime/Statistics/Histograms/Histogram.cs @@ -0,0 +1,227 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace TestHelper.Statistics.Histograms +{ + /// + /// Plot samples to histogram. + /// + /// + public class Histogram where T : IComparable + { + /// + /// Bins of this histogram. + /// + public SortedList> Bins { get; internal set; } + + /// + /// Peak frequency of this histogram. + /// This property is set in the Calculate() method. + /// + public uint Peak { get; private set; } + + /// + /// Valley frequency of this histogram. + /// This property is set in the Calculate() method. + /// + public uint Valley { get; private set; } + + /// + /// Median frequency of this histogram. + /// This property is set in the Calculate() method. + /// + public double Median { get; private set; } + + /// + /// Mean (average) frequency of this histogram. + /// This property is set in the Calculate() method. + /// + public double Mean { get; private set; } + + // only used in summary + private uint _sampleSize; + private T _sampleMax; + private T _sampleMin; + + // lower bound for character-based histogram + private uint _valleyGraterThanZero; + + /// + /// Constructor that creates initial bins. + /// Can only be used with numeric type. + /// + /// + /// + /// + public Histogram(T min, T max, double binSize = 1d) + { + var minValue = ToDouble(min); + var maxValue = ToDouble(max); + var distance = maxValue - minValue; + var binCount = (int)Math.Ceiling(distance / binSize); + + Bins = new SortedList>(binCount); + for (var i = 0; i < binCount; i++) + { + var bin = new Bin(ToT(minValue), ToT(minValue + binSize)); + Bins.Add(ToT(minValue), bin); + minValue += binSize; + } + } + + /// + /// Constructor without initial bins. + /// + public Histogram() + { + Bins = new SortedList>(); + } + + /// + /// Plot samples into bins. + /// + /// Input samples + /// Sample size; only used in summary + /// Minimum value in samples; only used in summary + /// Maximum value in samples; only used in summary + public void Plot(IEnumerable samples, uint size = 0, T min = default, T max = default) + { + _sampleSize = size; + _sampleMax = max; + _sampleMin = min; + + foreach (var current in samples) + { + var bin = FindBin(current); + if (bin == null) + { + bin = new Bin(current); + Bins.Add(current, bin); + } + + bin.Frequency++; + } + + Calculate(); + } + + /// + /// Plot samples into bins. + /// + /// Input sample space + public void Plot(SampleSpace sampleSpace) + { + Plot(sampleSpace.Samples, (uint)sampleSpace.Samples.Length, sampleSpace.Min, sampleSpace.Max); + } + + private Bin FindBin(T value) + { + var left = 0; + var right = Bins.Count - 1; + + while (left <= right) + { + var mid = (left + right) / 2; + var bin = Bins.Values[mid]; + + if (bin.IsInRange(value)) + { + return bin; + } + + if (value.CompareTo(bin.Min) < 0) + { + right = mid - 1; + } + else + { + left = mid + 1; + } + } + + return null; + } + + internal void Calculate() + { + var frequencies = new List(); + Mean = 0; + + foreach (var bin in Bins.Values) + { + frequencies.Add(bin.Frequency); + Mean += (double)bin.Frequency / Bins.Count; + } + + frequencies.Sort(); + Peak = frequencies[frequencies.Count - 1]; + Valley = frequencies[0]; + Median = frequencies.Count % 2 == 0 + ? (double)(frequencies[frequencies.Count / 2 - 1] + frequencies[frequencies.Count / 2]) / 2 + : frequencies[frequencies.Count / 2]; + + foreach (var frequency in frequencies.Where(frequency => frequency > 0)) + { + _valleyGraterThanZero = frequency; + break; + } + } + + internal string DrawHistogramAscii() + { + var builder = new StringBuilder(); + var blockHeight = (double)(Peak - _valleyGraterThanZero) / 7; + + foreach (var bin in Bins.Values) + { + if (bin.Frequency > 0) + { + var block = blockHeight > 0 + ? 0x2581 + (int)((bin.Frequency - _valleyGraterThanZero) / blockHeight) + : 0x2588; // full block + builder.Append(char.ConvertFromUtf32(block)); + } + else + { + builder.Append((char)0x20); // space + } + } + + return builder.ToString(); + } + + /// + /// Returns statistical summaries and a character-based histogram. + /// + /// + public string GetSummary() + { + var builder = new StringBuilder("---\nExperimental and Statistical Summary:\n"); + builder.AppendLine($" Sample size: {_sampleSize:N0}"); + builder.AppendLine($" Maximum: {_sampleMax}"); // No format, may not be a numeric type. + builder.AppendLine($" Minimum: {_sampleMin}"); // No format, may not be a numeric type. + builder.AppendLine($" Peak frequency: {Peak:N0}"); + builder.AppendLine($" Valley frequency: {Valley:N0}"); + builder.AppendLine($" Median: {Median:N0}"); + builder.AppendLine($" Mean: {Mean:N2}"); + builder.AppendLine($" Histogram: {DrawHistogramAscii()}"); + builder.AppendLine(" (Each bar represents the frequency of values in equally spaced bins.)"); + return builder.ToString(); + } + + private static double ToDouble(T value) + { + return (double)Convert.ChangeType(value, typeof(double)); + } + + private static T ToT(double value) + { + return (T)Convert.ChangeType(value, typeof(T)); + } + } +} diff --git a/Runtime/Statistics/Histograms/Histogram.cs.meta b/Runtime/Statistics/Histograms/Histogram.cs.meta new file mode 100644 index 0000000..23a8b13 --- /dev/null +++ b/Runtime/Statistics/Histograms/Histogram.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ec950564375d4f5a8827cb51989bffdc +timeCreated: 1736665233 \ No newline at end of file diff --git a/Runtime/Statistics/SampleSpace.cs b/Runtime/Statistics/SampleSpace.cs new file mode 100644 index 0000000..f22031e --- /dev/null +++ b/Runtime/Statistics/SampleSpace.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using System; +using System.Text; + +namespace TestHelper.Statistics +{ + /// + /// Sample space; return of Experiment.Run(). + /// + public struct SampleSpace where T : IComparable + { + /// + /// Samples. + /// + public T[] Samples { get; } + + /// + /// Returns min value of the samples. + /// + public T Min { get; private set; } + + /// + /// Returns max value of the samples. + /// + public T Max { get; private set; } + + private int _trailIndex; + + /// + /// Constructor. + /// + /// + public SampleSpace(uint trailCount) + { + Samples = new T[trailCount]; + Min = default; + Max = default; + _trailIndex = 0; + } + + internal void Add(T value) + { + Samples[_trailIndex++] = value; + + if (_trailIndex == 1) + { + Min = value; + Max = value; + return; + } + + if (Min.CompareTo(value) > 0) + { + Min = value; + } + + if (Max.CompareTo(value) < 0) + { + Max = value; + } + } + + /// + public override string ToString() + { + if (_trailIndex == 0) + { + return "{}"; + } + + var builder = new StringBuilder("{"); + builder.Append(Samples[0]); + for (var i = 1; i < _trailIndex; i++) + { + builder.Append(","); + builder.Append(Samples[i]); + } + + return builder.Append("}").ToString(); + } + } +} diff --git a/Runtime/Statistics/SampleSpace.cs.meta b/Runtime/Statistics/SampleSpace.cs.meta new file mode 100644 index 0000000..b9c3d9d --- /dev/null +++ b/Runtime/Statistics/SampleSpace.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 606d2e31b0eb47a494fec9675a9a69fe +timeCreated: 1736614328 \ No newline at end of file diff --git a/Tests/Runtime/Statistics.meta b/Tests/Runtime/Statistics.meta new file mode 100644 index 0000000..836c3b6 --- /dev/null +++ b/Tests/Runtime/Statistics.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: eb9e92f15fda42ed8b852b5960011027 +timeCreated: 1736635581 \ No newline at end of file diff --git a/Tests/Runtime/Statistics/Comparers.meta b/Tests/Runtime/Statistics/Comparers.meta new file mode 100644 index 0000000..a9c2e9d --- /dev/null +++ b/Tests/Runtime/Statistics/Comparers.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: adfaad2e44f047d9b9b895fe6a7e7a5b +timeCreated: 1736728323 \ No newline at end of file diff --git a/Tests/Runtime/Statistics/Comparers/BinCountComparer.cs b/Tests/Runtime/Statistics/Comparers/BinCountComparer.cs new file mode 100644 index 0000000..b8ff91c --- /dev/null +++ b/Tests/Runtime/Statistics/Comparers/BinCountComparer.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using TestHelper.Statistics.Histograms; +using UnityEngine; + +namespace TestHelper.Statistics.Comparers +{ + /// + /// Compare two Bin by Min, Max, and Frequency. + /// + /// + public class BinCountComparer : IComparer> where T : IComparable + { + /// + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + public int Compare(Bin x, Bin y) + { + var min = x.Min.CompareTo(y.Min); + if (min != 0) + { + Log($"Min={x.Min}", $"Min={y.Min}"); + return min; + } + + var max = x.Max.CompareTo(y.Max); + if (max != 0) + { + Log($"Max={x.Max}", $"Max={y.Max}"); + return max; + } + + var frequency = x.Frequency.CompareTo(y.Frequency); + if (frequency != 0) + { + Log($"Frequency={x.Frequency}", $"Frequency={y.Frequency}"); + } + + return frequency; + } + + private static void Log(string expected, string actual) + { + Debug.Log($" Expected: {expected}\n But was: {actual}"); + } + } +} diff --git a/Tests/Runtime/Statistics/Comparers/BinCountComparer.cs.meta b/Tests/Runtime/Statistics/Comparers/BinCountComparer.cs.meta new file mode 100644 index 0000000..7e16147 --- /dev/null +++ b/Tests/Runtime/Statistics/Comparers/BinCountComparer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3d4e36a1cc884ad2b0874db5cebbdd7e +timeCreated: 1736728360 \ No newline at end of file diff --git a/Tests/Runtime/Statistics/ExperimentTest.cs b/Tests/Runtime/Statistics/ExperimentTest.cs new file mode 100644 index 0000000..e48332f --- /dev/null +++ b/Tests/Runtime/Statistics/ExperimentTest.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using NUnit.Framework; +using TestHelper.Statistics.RandomGenerators; + +namespace TestHelper.Statistics +{ + [TestFixture] + public class ExperimentTest + { + private const int TrailCount = 1 << 20; + + [Test] + public void Run_Enum_ReturnsSampleSpace() + { + var actual = Experiment.Run(() => CoinGenerator.Toss(), TrailCount); + + Assert.That(actual.Min, Is.EqualTo(Coin.Head)); + Assert.That(actual.Max, Is.EqualTo(Coin.Tail)); + } + + [Test] + public void Run_Int_ReturnsSampleSpace() + { + var actual = Experiment.Run(() => DiceGenerator.Roll(2, 6), TrailCount); + + Assert.That(actual.Min, Is.EqualTo(2)); + Assert.That(actual.Max, Is.EqualTo(12)); + } + } +} diff --git a/Tests/Runtime/Statistics/ExperimentTest.cs.meta b/Tests/Runtime/Statistics/ExperimentTest.cs.meta new file mode 100644 index 0000000..6aa1fee --- /dev/null +++ b/Tests/Runtime/Statistics/ExperimentTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9f7e9570fa28440ba157349c3fb00610 +timeCreated: 1736617008 \ No newline at end of file diff --git a/Tests/Runtime/Statistics/Histograms.meta b/Tests/Runtime/Statistics/Histograms.meta new file mode 100644 index 0000000..6e1b004 --- /dev/null +++ b/Tests/Runtime/Statistics/Histograms.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a88345a9713546a69054153a1a2b1019 +timeCreated: 1736673133 \ No newline at end of file diff --git a/Tests/Runtime/Statistics/Histograms/BinCountComparerTest.cs b/Tests/Runtime/Statistics/Histograms/BinCountComparerTest.cs new file mode 100644 index 0000000..ae454e1 --- /dev/null +++ b/Tests/Runtime/Statistics/Histograms/BinCountComparerTest.cs @@ -0,0 +1,36 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using NUnit.Framework; +using TestHelper.Statistics.Comparers; + +namespace TestHelper.Statistics.Histograms +{ + [TestFixture] + public class BinCountComparerTest + { + [Test] + public void Compare_Success() + { + var expected = new Bin(0, 10) { Frequency = 100 }; + var actual = new Bin(0, 10) { Frequency = 100 }; + Assert.That(actual, Is.EqualTo(expected).Using(new BinCountComparer())); + } + + [TestCase(-1, 10, 100u)] // different min + [TestCase(1, 10, 100u)] // different min + [TestCase(0, 9, 100u)] // different max + [TestCase(0, 11, 100u)] // different max + [TestCase(0, 10, 99u)] // different count + [TestCase(0, 10, 101u)] // different count + public void Compare_Failure(int min, int max, uint frequency) + { + var expected = new Bin(0, 10) { Frequency = 100 }; + var actual = new Bin(min, max) { Frequency = frequency }; + Assert.That(() => + { + Assert.That(actual, Is.EqualTo(expected).Using(new BinCountComparer())); + }, Throws.TypeOf()); + } + } +} diff --git a/Tests/Runtime/Statistics/Histograms/BinCountComparerTest.cs.meta b/Tests/Runtime/Statistics/Histograms/BinCountComparerTest.cs.meta new file mode 100644 index 0000000..84a3424 --- /dev/null +++ b/Tests/Runtime/Statistics/Histograms/BinCountComparerTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 28786b851433432797609a65420ef3e0 +timeCreated: 1736729742 \ No newline at end of file diff --git a/Tests/Runtime/Statistics/Histograms/BinTest.cs b/Tests/Runtime/Statistics/Histograms/BinTest.cs new file mode 100644 index 0000000..2ba6cbb --- /dev/null +++ b/Tests/Runtime/Statistics/Histograms/BinTest.cs @@ -0,0 +1,68 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using NUnit.Framework; +using TestHelper.Statistics.RandomGenerators; + +namespace TestHelper.Statistics.Histograms +{ + [TestFixture] + public class BinTest + { + [Test] + public void Constructor_Bool() + { + var actual = new Bin(true); + Assert.That(actual.Min, Is.EqualTo(true)); + Assert.That(actual.Max, Is.EqualTo(true)); + Assert.That(actual.Label, Is.EqualTo("True")); + } + + [Test] + public void Constructor_Decimal() + { + var actual = new Bin(1); + Assert.That(actual.Min, Is.EqualTo(1)); + Assert.That(actual.Max, Is.EqualTo(1)); + Assert.That(actual.Label, Is.EqualTo("1")); + } + + [Test] + public void Constructor_DecimalWithRange() + { + var actual = new Bin(1, 2); + Assert.That(actual.Min, Is.EqualTo(1)); + Assert.That(actual.Max, Is.EqualTo(2)); + Assert.That(actual.Label, Is.EqualTo("1")); + } + + [Test] + public void Constructor_Enum() + { + var actual = new Bin(Coin.Head); + Assert.That(actual.Min, Is.EqualTo(Coin.Head)); + Assert.That(actual.Max, Is.EqualTo(Coin.Head)); + Assert.That(actual.Label, Is.EqualTo("Head")); + } + + [TestCase(0, false)] + [TestCase(1, true)] + [TestCase(2, false)] + public void IsInRange_HasNotRange(int value, bool expected) + { + var sut = new Bin(1); + Assert.That(sut.IsInRange(value), Is.EqualTo(expected)); + } + + [TestCase(0, false)] + [TestCase(1, true)] + [TestCase(2, true)] + [TestCase(3, false)] + [TestCase(4, false)] + public void IsInRange_HasRange(int value, bool expected) + { + var sut = new Bin(1, 3); + Assert.That(sut.IsInRange(value), Is.EqualTo(expected)); + } + } +} diff --git a/Tests/Runtime/Statistics/Histograms/BinTest.cs.meta b/Tests/Runtime/Statistics/Histograms/BinTest.cs.meta new file mode 100644 index 0000000..4d41385 --- /dev/null +++ b/Tests/Runtime/Statistics/Histograms/BinTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0dcc9c913661453e84b866c47c69e3b8 +timeCreated: 1736674082 \ No newline at end of file diff --git a/Tests/Runtime/Statistics/Histograms/HistogramTest.cs b/Tests/Runtime/Statistics/Histograms/HistogramTest.cs new file mode 100644 index 0000000..4149bc9 --- /dev/null +++ b/Tests/Runtime/Statistics/Histograms/HistogramTest.cs @@ -0,0 +1,220 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using TestHelper.Statistics.Comparers; +using TestHelper.Statistics.RandomGenerators; +using UnityEngine; + +namespace TestHelper.Statistics.Histograms +{ + [TestFixture] + [SuppressMessage("Assertion", "NUnit2045:Use Assert.Multiple")] + public class HistogramTest + { + [Test] + public void Constructor_Double() + { + var actual = new Histogram(0.0d, 1.0d, 0.5d); + var expected = new SortedList> + { + { 0.0d, new Bin(0.0d, 0.5d) }, // + { 0.5d, new Bin(0.5d, 1.0d) } + }; + Assert.That(actual.Bins, Is.EquivalentTo(expected).Using(new BinCountComparer())); + } + + [Test] + public void Constructor_Int() + { + var actual = new Histogram(0, 10, 2); + var expected = new SortedList> + { + { 0, new Bin(0, 2) }, // + { 2, new Bin(2, 4) }, // + { 4, new Bin(4, 6) }, // + { 6, new Bin(6, 8) }, // + { 8, new Bin(8, 10) } + }; + Assert.That(actual.Bins, Is.EquivalentTo(expected).Using(new BinCountComparer())); + } + + [Test] + public void Constructor_WithoutInitialBins() + { + var actual = new Histogram(); + Assert.That(actual.Bins, Is.Empty); + } + + [Test] + public void Plot_WithoutRange() + { + var sut = new Histogram(); + sut.Plot(new[] { Coin.Head, Coin.Tail, Coin.Tail }); + + var expected = new List> + { + new Bin(Coin.Head) { Frequency = 1 }, // + new Bin(Coin.Tail) { Frequency = 2 } + }; + Assert.That(sut.Bins.Values, Is.EquivalentTo(expected).Using(new BinCountComparer())); + } + + [Test] + public void Plot_WithRange() + { + var sut = new Histogram(0, 10, 2); + sut.Plot(new[] { 2, 3, 4, 4, 5, 5, 9 }); + + var expected = new List> + { + new Bin(0, 2) { Frequency = 0 }, // + new Bin(2, 4) { Frequency = 2 }, // + new Bin(4, 6) { Frequency = 4 }, // + new Bin(6, 8) { Frequency = 0 }, // + new Bin(8, 10) { Frequency = 1 } + }; + Assert.That(sut.Bins.Values, Is.EquivalentTo(expected).Using(new BinCountComparer())); + } + + [Test] + public void Calculate_Odd_CalculatedFeaturesSetToProperties() + { + var sut = new Histogram + { + Bins = new SortedList> + { + { 0, new Bin(0) { Frequency = 4 } }, // Median + { 1, new Bin(1) { Frequency = 6 } }, // + { 2, new Bin(2) { Frequency = 2 } }, // + { 3, new Bin(3) { Frequency = 10 } }, // Peak + { 4, new Bin(4) { Frequency = 0 } }, // Valley + } + }; + sut.Calculate(); + + Assert.That(sut.Peak, Is.EqualTo(10)); + Assert.That(sut.Valley, Is.EqualTo(0)); + Assert.That(sut.Median, Is.EqualTo(4)); + Assert.That(sut.Mean, Is.EqualTo(4.4)); // 22 / 5 + } + + [Test] + public void Calculate_Even_CalculatedFeaturesSetToProperties() + { + var sut = new Histogram + { + Bins = new SortedList> + { + { 0, new Bin(0) { Frequency = 4 } }, // + { 1, new Bin(1) { Frequency = 6 } }, // + { 2, new Bin(2) { Frequency = 2 } }, // + { 3, new Bin(3) { Frequency = 10 } }, // Peak + { 4, new Bin(4) { Frequency = 0 } }, // Valley + { 5, new Bin(5) { Frequency = 3 } }, // + } + }; + sut.Calculate(); + + Assert.That(sut.Peak, Is.EqualTo(10)); + Assert.That(sut.Valley, Is.EqualTo(0)); + Assert.That(sut.Median, Is.EqualTo(3.5)); // (3 + 4) / 2 + Assert.That(sut.Mean, Is.EqualTo(4.17).Within(0.01)); // 25 / 6 + } + + [Test] + public void DrawHistogramAscii() + { + var sut = new Histogram + { + Bins = new SortedList> + { + { 0, new Bin(0) { Frequency = 100 } }, // lower 1:8 block + { 1, new Bin(1) { Frequency = 110 } }, // lower 1:4 block + { 2, new Bin(2) { Frequency = 120 } }, // lower 3:8 block + { 3, new Bin(3) { Frequency = 130 } }, // lower 1:2 block + { 4, new Bin(4) { Frequency = 140 } }, // lower 5:8 block + { 5, new Bin(5) { Frequency = 150 } }, // lower 3:4 block + { 6, new Bin(6) { Frequency = 160 } }, // lower 7:8 block + { 7, new Bin(7) { Frequency = 170 } }, // full block + } + }; + sut.Calculate(); + var actual = sut.DrawHistogramAscii(); + + Assert.That(actual, Is.EqualTo("\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588")); + } + + [Test] + public void DrawHistogramAscii_OneValue_DrawFullBlock() + { + var sut = new Histogram + { + Bins = new SortedList> + { + { 0, new Bin(0) { Frequency = 100 } }, // full block + { 1, new Bin(1) { Frequency = 100 } }, // full block + } + }; + sut.Calculate(); + var actual = sut.DrawHistogramAscii(); + + Assert.That(actual, Is.EqualTo("\u2588\u2588")); + } + + [Test] + public void DrawHistogramAscii_ZeroValue_DrawSpace() + { + var sut = new Histogram + { + Bins = new SortedList> + { + { 0, new Bin(0) { Frequency = 1000 } }, // full block + { 1, new Bin(1) { Frequency = 0 } }, // space + { 2, new Bin(2) { Frequency = 1 } }, // lower 1:8 block + } + }; + sut.Calculate(); + var actual = sut.DrawHistogramAscii(); + + Assert.That(actual, Is.EqualTo("\u2588 \u2581")); + } + + [Test] + public void GetSummary() + { + var sut = new Histogram(0, 10, 2); + sut.Plot(new[] { 2, 3, 4, 4, 5, 5, 9 }, 7, 2, 9); + + var actual = sut.GetSummary(); + Debug.Log(actual); + + Assert.That(actual, Is.EqualTo(@"--- +Experimental and Statistical Summary: + Sample size: 7 + Maximum: 9 + Minimum: 2 + Peak frequency: 4 + Valley frequency: 0 + Median: 1 + Mean: 1.40 + Histogram: ▃█ ▁ + (Each bar represents the frequency of values in equally spaced bins.) +")); + } + + [Test] + public void GetSummary_WithSampleSpace() + { + var sampleSpace = Experiment.Run( + () => DiceGenerator.Roll(2, 6), // 2D6 + 1 << 20); // 1,048,576 times + + var histogram = new Histogram(); + histogram.Plot(sampleSpace); + Debug.Log(histogram.GetSummary()); + } + } +} diff --git a/Tests/Runtime/Statistics/Histograms/HistogramTest.cs.meta b/Tests/Runtime/Statistics/Histograms/HistogramTest.cs.meta new file mode 100644 index 0000000..afd9e82 --- /dev/null +++ b/Tests/Runtime/Statistics/Histograms/HistogramTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4f9311ed5e7441dc8646444679644e65 +timeCreated: 1736673144 \ No newline at end of file diff --git a/Tests/Runtime/Statistics/RandomGenerators.meta b/Tests/Runtime/Statistics/RandomGenerators.meta new file mode 100644 index 0000000..8170b67 --- /dev/null +++ b/Tests/Runtime/Statistics/RandomGenerators.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ad8ec0d0b1e34178a0d8179a9d69083b +timeCreated: 1736621097 \ No newline at end of file diff --git a/Tests/Runtime/Statistics/RandomGenerators/CoinGenerator.cs b/Tests/Runtime/Statistics/RandomGenerators/CoinGenerator.cs new file mode 100644 index 0000000..b6c318a --- /dev/null +++ b/Tests/Runtime/Statistics/RandomGenerators/CoinGenerator.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using UnityEngine; + +namespace TestHelper.Statistics.RandomGenerators +{ + public enum Coin + { + Head, + Tail + } + + public static class CoinGenerator + { + public static Coin Toss() + { + return Random.value < 0.5f ? Coin.Head : Coin.Tail; + } + } +} diff --git a/Tests/Runtime/Statistics/RandomGenerators/CoinGenerator.cs.meta b/Tests/Runtime/Statistics/RandomGenerators/CoinGenerator.cs.meta new file mode 100644 index 0000000..7e393d1 --- /dev/null +++ b/Tests/Runtime/Statistics/RandomGenerators/CoinGenerator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3d3349fa00294b7588b43c8c318b1389 +timeCreated: 1736621162 \ No newline at end of file diff --git a/Tests/Runtime/Statistics/RandomGenerators/DiceGenerator.cs b/Tests/Runtime/Statistics/RandomGenerators/DiceGenerator.cs new file mode 100644 index 0000000..d804153 --- /dev/null +++ b/Tests/Runtime/Statistics/RandomGenerators/DiceGenerator.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using UnityEngine; + +namespace TestHelper.Statistics.RandomGenerators +{ + public static class DiceGenerator + { + private static int Roll(int sides) + { + return Random.Range(1, sides + 1); + } + + public static int Roll(int dice, int sides) + { + var sum = 0; + for (var i = 0; i < dice; i++) + { + sum += Roll(sides); + } + + return sum; + } + } +} diff --git a/Tests/Runtime/Statistics/RandomGenerators/DiceGenerator.cs.meta b/Tests/Runtime/Statistics/RandomGenerators/DiceGenerator.cs.meta new file mode 100644 index 0000000..e1a7db7 --- /dev/null +++ b/Tests/Runtime/Statistics/RandomGenerators/DiceGenerator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2fe08b33128a4ceeaf987d7a226a55e1 +timeCreated: 1736636224 \ No newline at end of file diff --git a/Tests/Runtime/Statistics/SampleSpaceTest.cs b/Tests/Runtime/Statistics/SampleSpaceTest.cs new file mode 100644 index 0000000..7a9c2ed --- /dev/null +++ b/Tests/Runtime/Statistics/SampleSpaceTest.cs @@ -0,0 +1,54 @@ +// Copyright (c) 2023-2025 Koji Hasegawa. +// This software is released under the MIT License. + +using NUnit.Framework; +using TestHelper.Statistics.RandomGenerators; + +namespace TestHelper.Statistics +{ + [TestFixture] + public class SampleSpaceTest + { + [Test] + public void ToString_Empty() + { + var sampleSpace = new SampleSpace(0); + Assert.That(sampleSpace.ToString(), Is.EqualTo("{}")); + } + + [Test] + public void ToString_Single() + { + var sampleSpace = new SampleSpace(1); + sampleSpace.Add(1); + Assert.That(sampleSpace.ToString(), Is.EqualTo("{1}")); + } + + [Test] + public void ToString_Multiple() + { + var sampleSpace = new SampleSpace(2); + sampleSpace.Add(1); + sampleSpace.Add(2); + Assert.That(sampleSpace.ToString(), Is.EqualTo("{1,2}")); + } + + [Test] + public void ToString_Bool() + { + var sampleSpace = new SampleSpace(2); + sampleSpace.Add(false); + sampleSpace.Add(true); + Assert.That(sampleSpace.ToString(), Is.EqualTo("{False,True}")); + } + + [Test] + public void ToString_Enum() + { + var sampleSpace = new SampleSpace(2); + sampleSpace.Add(Coin.Head); + sampleSpace.Add(Coin.Tail); + Assert.That(sampleSpace.ToString(), Is.EqualTo("{Head,Tail}")); + } + } +} diff --git a/Tests/Runtime/Statistics/SampleSpaceTest.cs.meta b/Tests/Runtime/Statistics/SampleSpaceTest.cs.meta new file mode 100644 index 0000000..0415885 --- /dev/null +++ b/Tests/Runtime/Statistics/SampleSpaceTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 75c6d19797bc482abc900924a244901f +timeCreated: 1736620457 \ No newline at end of file