diff --git a/BotSharp.Algorithm.UnitTest/BayesianTest.cs b/BotSharp.Algorithm.UnitTest/BayesianTest.cs new file mode 100644 index 00000000..1665a43b --- /dev/null +++ b/BotSharp.Algorithm.UnitTest/BayesianTest.cs @@ -0,0 +1,201 @@ +using BotSharp.Algorithm.Bayesian; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BotSharp.Algorithm.UnitTest +{ + [TestClass] + public class BayesianTest + { + /// + /// The training set + /// + private ITrainingSet _trainingSet; + + /// + /// The classifier + /// + private IClassifier _classifier; + + /// + /// The spam class + /// + private static IClass _spamClass; + + /// + /// The ham class + /// + private static IClass _hamClass; + + /// + /// Sets up. + /// + public void SetUp() + { + _trainingSet = BuildTrainingSet(); + _classifier = BuildClassifier(_trainingSet); + } + + /// + /// Builds the classifier. + /// + /// Classifier<StringClass, StringToken>. + private IClassifier BuildClassifier(ITrainingSetAccessor trainingSet) + { + var classifier = new NaiveClassifier(trainingSet) + { + // disable smoothing for exact probabilities + SmoothingAlpha = 0.0D + }; + + return classifier; + } + + /// + /// Builds the training set. + /// + /// ITrainingSet<StringClass, StringToken>. + private static ITrainingSet BuildTrainingSet() + { + var trainingSet = new TrainingSet(); + + // build data sets + var spamSet = BuildSpamDataSet(); + var hamSet = BuildHamDataSet(); + + // monkey test + //spamSet.SetSize.Should() + //.Be(hamSet.SetSize, "because this test relies on identical set sizes for exact probability testing"); + + // register classes + _spamClass = spamSet.Class; + _hamClass = hamSet.Class; + + // add the sets and return + trainingSet.Add(spamSet, hamSet); + return trainingSet; + } + + /// + /// Builds the spam data set. + /// + /// IDataSet<StringClass, StringToken>. + private static IDataSet BuildSpamDataSet() + { + return BuildDataSet("spam", 0.5D, "rolex", "watches", "viagra", "prince", "money", "send", "xyzzy"); + } + + /// + /// Builds the spam data set. + /// + /// IDataSet<StringClass, StringToken>. + private static IDataSet BuildHamDataSet() + { + return BuildDataSet("ham", 0.5D, "love", "flowers", "unicorn", "friendship", "money", "send", "send"); + } + + /// + /// Builds the data set. + /// + /// Name of the class. + /// The class probability. + /// The token. + /// The additional tokens. + /// IDataSet<StringClass, StringToken>. + private static IDataSet BuildDataSet(string className, double classProbability, string token, params string[] additionalTokens) + { + var @class = new StringClass(className, classProbability); + var dataSet = new DataSet(@class); + + dataSet.AddToken(new StringToken(token)); + dataSet.AddToken(additionalTokens.Select(t => new StringToken(t))); + + return dataSet; + } + + [TestMethod] + public void CalculateProbabilityReturnsOneHundredPercentForAKnownSpamWord() + { + var token = new StringToken("rolex"); + + var probability = _classifier.CalculateProbability(_spamClass, token); + //probability.Should().BeApproximately(1.0D, 0.0001D, "because the word is known the be a spam word"); + } + + [TestMethod] + public void CalculateProbabilityReturnsOneHundredPercentForAKnownHamWord() + { + var token = new StringToken("unicorn"); + + var probability = _classifier.CalculateProbability(_hamClass, token); + //probability.Should().BeApproximately(1.0D, 0.0001D, "because the word is known the be a ham word"); + } + + [TestMethod] + public void CalculateProbabilitiesWithHamWordReturnsProbabilitiesForAllClasses() + { + var token = new StringToken("unicorn"); + + var probabilities = _classifier.CalculateProbabilities(token).ToList(); + /*probabilities.Single(p => p.Class.Equals(_spamClass)) + .Probability.Should() + .BeApproximately(0D, 0.000001D, "because the token is known to be a ham word"); + + probabilities.Single(p => p.Class.Equals(_hamClass)) + .Probability.Should() + .BeApproximately(1D, 0.000001D, "because the token is known to be a ham word");*/ + } + + [TestMethod] + public void CalculateProbabilitiesWithMixedWordReturnsProbabilitiesForAllClasses() + { + var token = new StringToken("money"); + + var probabilities = _classifier.CalculateProbabilities(token).ToList(); + /*probabilities.Single(p => p.Class.Equals(_spamClass)) + .Probability.Should() + .BeApproximately(0.5D, 0.000001D, "because the token is known to be a ham and spam word"); + + probabilities.Single(p => p.Class.Equals(_hamClass)) + .Probability.Should() + .BeApproximately(0.5D, 0.000001D, "because the token is known to be a ham and spam word");*/ + } + + [TestMethod] + public void CalculateProbabilitiesWithMixedWordThatIsMoreLikelyHamThanSpamReturnsProbabilitiesForAllClasses() + { + var token = new StringToken("send"); + + var probabilities = _classifier.CalculateProbabilities(token).ToList(); + /*probabilities.Single(p => p.Class.Equals(_spamClass)) + .Probability.Should() + .BeApproximately(1 / 3D, 0.000001D, "because the token is more likely to be a ham than spam word"); + + probabilities.Single(p => p.Class.Equals(_hamClass)) + .Probability.Should() + .BeApproximately(2 / 3D, 0.000001D, "because the token is more likely to be a ham than spam word");*/ + } + + [TestMethod] + public void CalculateProbabilitiesWithRareTokensAndSmoothingAlphaIsUnambiguous() + { + var token1 = new StringToken("rolex"); + var token2 = new StringToken("unicorn"); + var token3 = new StringToken("send"); + + const double smoothingAlpha = 1.0D; + var probabilities = _classifier.CalculateProbabilities(new IToken[] { token1, token2, token3 }, smoothingAlpha).ToList(); + + /*probabilities.Single(p => p.Class.Equals(_spamClass)) + .Probability.Should() + .BeLessThan(0.5D, "because we used more ham than spam tokens"); + + probabilities.Single(p => p.Class.Equals(_hamClass)) + .Probability.Should() + .BeGreaterThan(0.5D, "because we used more ham than spam tokens");*/ + } + } +} diff --git a/BotSharp.Algorithm/Bayesian/ClassBase.cs b/BotSharp.Algorithm/Bayesian/ClassBase.cs new file mode 100644 index 00000000..47e95e6a --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/ClassBase.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics; + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Class ClassBase. + /// + [DebuggerDisplay("Class {Name}, base P = {Probability}")] + public abstract class ClassBase : IClass + { + /// + /// Gets the name. + /// + /// The name. + public string Name { get; private set; } + + /// + /// Gets the class' base probability. + /// + /// The probability. + public double Probability { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// The probability. + /// name + /// + /// probability;Class base probability must be greater than or equal to zero. + /// or + /// probability;Class base probability must be less than or equal to one. + /// + protected ClassBase(string name, double probability) + { + if (ReferenceEquals(name, null)) throw new ArgumentNullException("name"); + if (probability < 0) throw new ArgumentOutOfRangeException("probability", "Class base probability must be greater than or equal to zero."); + if (probability > 1) throw new ArgumentOutOfRangeException("probability", "Class base probability must be less than or equal to one."); + + Name = name; + Probability = probability; + } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// true if the current object is equal to the parameter; otherwise, false. + public abstract bool Equals(IClass other); + } +} \ No newline at end of file diff --git a/BotSharp.Algorithm/Bayesian/CombinedConditionalProbabilities.cs b/BotSharp.Algorithm/Bayesian/CombinedConditionalProbabilities.cs new file mode 100644 index 00000000..f5ee13fe --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/CombinedConditionalProbabilities.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Struct CombinedConditionalProbability + /// + [DebuggerDisplay("P({Class}|{TokenProbabilities.Count} tokens)={Probability}")] + public struct CombinedConditionalProbability + { + /// + /// The class + /// + public readonly IClass Class; + + /// + /// The token probabilities + /// + public ICollection TokenProbabilities; + + /// + /// The probability + /// + public double Probability; + + /// + /// Initializes a new instance of the struct. + /// + /// The class. + /// The probability. + /// The tokenProbabilities. + public CombinedConditionalProbability(IClass @class, double probability, ICollection tokenProbabilities) + { + if (ReferenceEquals(@class, null)) throw new ArgumentNullException("class"); + if (ReferenceEquals(tokenProbabilities, null)) throw new ArgumentNullException("tokenProbabilities"); + if (probability < 0) throw new ArgumentOutOfRangeException("probability", probability, "Probability must greater than or equal to zero"); + if (probability > 1) throw new ArgumentOutOfRangeException("probability", probability, "Probability must less than or equal to one"); + + Class = @class; + TokenProbabilities = tokenProbabilities; + Probability = probability; + } + } +} diff --git a/BotSharp.Algorithm/Bayesian/ConditionalProbability.cs b/BotSharp.Algorithm/Bayesian/ConditionalProbability.cs new file mode 100644 index 00000000..a1788c78 --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/ConditionalProbability.cs @@ -0,0 +1,104 @@ +using System; +using System.Diagnostics; + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Struct ConditionalProbability + /// + [DebuggerDisplay("P({Class}|{Token})={Probability}")] + public struct ConditionalProbability : IEquatable + { + /// + /// The class + /// + public readonly IClass Class; + + /// + /// The token + /// + public readonly IToken Token; + + /// + /// The conditional probability + /// + public readonly double Probability; + + /// + /// The occurrence of the token during the training phase. + /// + public readonly long Occurrence; + + /// + /// Initializes a new instance of the struct. + /// + /// The class. + /// The token. + /// The probability. + /// The occurrence. + /// @class + /// or + /// token + /// probability;Probability must greater than or equal to zero + /// or + /// probability;Probability must less than or equal to one + public ConditionalProbability(IClass @class, IToken token, double probability, long occurrence) + { + if (ReferenceEquals(@class, null)) throw new ArgumentNullException("class"); + if (ReferenceEquals(token, null)) throw new ArgumentNullException("token"); + if (probability < 0) throw new ArgumentOutOfRangeException("probability", probability, "Probability must greater than or equal to zero"); + if (probability > 1) throw new ArgumentOutOfRangeException("probability", probability, "Probability must less than or equal to one"); + if (probability < 0) throw new ArgumentOutOfRangeException("occurrence", occurrence, "Occurrence must greater than or equal to zero"); + + Class = @class; + Token = token; + Probability = probability; + Occurrence = occurrence; + } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// Another object to compare to. + /// if the specified is equal to this instance; otherwise, . + public override bool Equals(object obj) + { + if (ReferenceEquals(obj, null)) return false; + return obj is ConditionalProbability && Equals((ConditionalProbability) obj); + } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// true if the current object is equal to the parameter; otherwise, false. + public bool Equals(ConditionalProbability other) + { + return Class.Equals(other.Class) + && Token.Equals(other.Token) + && Probability.Equals(other.Probability); + } + + /// + /// Returns a hash code for this instance. + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + public override int GetHashCode() + { + var hash = 27; + hash = (13 * hash) + Class.GetHashCode(); + hash = (13 * hash) + Token.GetHashCode(); + hash = (13 * hash) + Probability.GetHashCode(); + return hash; + } + + /// + /// Returns a that represents this instance. + /// + /// A that represents this instance. + public override string ToString() + { + return String.Format("P({0}|{1})={2:P}", Class, Token, Probability); + } + } +} diff --git a/BotSharp.Algorithm/Bayesian/DataSet.cs b/BotSharp.Algorithm/Bayesian/DataSet.cs new file mode 100644 index 00000000..91a56ac7 --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/DataSet.cs @@ -0,0 +1,358 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Class DataSet. + /// + [DebuggerDisplay("Data set for class P({Class.Name})={Class.Probability}")] + public sealed class DataSet : IDataSet + { + /// + /// The default smoothing alpha + /// + public const double DefaultSmoothingAlpha = 0D; + + /// + /// The token count + /// + private readonly ConcurrentDictionary _tokenCount = new ConcurrentDictionary(); + + /// + /// The set size, i.e. the number of all tokens + /// + private long _setSize; + + /// + /// Gets the number of distinct tokens, + /// i.e. every token counted at exactly once. + /// + /// The token count. + /// + public long TokenCount + { + get { return _tokenCount.Count; } + } + + /// + /// Gets the size of the set. + /// + /// The size of the set. + /// + public long SetSize + { + get { return _setSize; } + } + + /// + /// Gets the class. + /// + /// The class. + public IClass Class { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The class. + /// @class + public DataSet(IClass @class) + { + if (ReferenceEquals(@class, null)) throw new ArgumentNullException("class"); + Class = @class; + } + + /// + /// Gets the with the specified token. + /// + /// The token. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// TokenInformation<IToken>. + /// token + public TokenInformation this[IToken token, double alpha = DefaultSmoothingAlpha] + { + get + { + if (ReferenceEquals(token, null)) throw new ArgumentNullException("token"); + + long count; + if (!_tokenCount.TryGetValue(token, out count)) + { + return new TokenInformation(token, 0L, 0D); + } + + var percentage = GetPercentage(count, alpha); + return new TokenInformation(token, count, percentage); + } + } + + /// + /// Gets the number of occurrences of the given token. + /// + /// The token. + /// System.Int64. + /// + public long GetCount(IToken token) + { + if (ReferenceEquals(token, null)) throw new ArgumentNullException("token"); + + long count; + return !_tokenCount.TryGetValue(token, out count) ? 0 : count; + } + + /// + /// Gets the approximated percentage of the given + /// in this data set + /// by determining its occurrence count over the whole population. + /// + /// The token. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// System.Double. + /// token + /// + public double GetPercentage(IToken token, double alpha = DefaultSmoothingAlpha) + { + if (ReferenceEquals(token, null)) throw new ArgumentNullException("token"); + if (alpha < 0) throw new ArgumentOutOfRangeException("alpha", alpha, "Smoothing parameter alpha must be greater than or equal to zero."); + + var count = GetCount(token); + return GetPercentage(count, alpha); + } + + /// + /// Gets the approximated percentage of the given + /// in this data set + /// by determining its occurrence count over the whole population. + /// + /// The token count. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// System.Double. + /// token + /// + private double GetPercentage(long tokenCount, double alpha = DefaultSmoothingAlpha) + { + Debug.Assert(alpha >= 0, "alpha >= 0"); + Debug.Assert(tokenCount >= 0, "tokenCount >= 0"); + + var totalCount = _setSize; // TODO: cache inverse set size + var vocabularySize = TokenCount; + + return (double)(tokenCount + alpha)/(double)(totalCount + alpha*vocabularySize); + } + + /// + /// Adds the given tokens a single time, incrementing the + /// and, at the first addition, the . + /// + /// The token. + /// The additional tokens. + /// + /// token + /// or + /// additionalTokens + /// + public void AddToken(IToken token, params IToken[] additionalTokens) + { + if (ReferenceEquals(token, null)) throw new ArgumentNullException("token"); + if (ReferenceEquals(additionalTokens, null)) throw new ArgumentNullException("additionalTokens"); + + _tokenCount.AddOrUpdate(token, AddFirsIToken, IncremenITokenCount); + Interlocked.Increment(ref _setSize); + + AddToken(additionalTokens); + } + + /// + /// Adds the given tokens a single time, incrementing the + /// and, at the first addition, the . + /// + /// The tokens. + /// tokens + public void AddToken(IEnumerable tokens) + { + if (ReferenceEquals(tokens, null)) throw new ArgumentNullException("tokens"); + + foreach (var token in tokens) + { + _tokenCount.AddOrUpdate(token, AddFirsIToken, IncremenITokenCount); + Interlocked.Increment(ref _setSize); + } + } + + /// + /// Removes the given tokens a single time, decrementing the and, + /// eventually, the . + /// + /// The token. + /// The additional tokens. + /// + /// token + /// or + /// additionalTokens + /// + /// + public void RemoveTokenOnce(IToken token, params IToken[] additionalTokens) + { + if (ReferenceEquals(token, null)) throw new ArgumentNullException("token"); + if (ReferenceEquals(additionalTokens, null)) throw new ArgumentNullException("additionalTokens"); + + RemoveSingleTokenInternal(token); + RemoveTokenOnce(additionalTokens); + } + + /// + /// Removes the given tokens a single time, decrementing the and, + /// eventually, the . + /// + /// The tokens. + /// tokens + /// + public void RemoveTokenOnce(IEnumerable tokens) + { + if (ReferenceEquals(tokens, null)) throw new ArgumentNullException("tokens"); + + foreach (var token in tokens) + { + RemoveSingleTokenInternal(token); + } + } + + /// + /// Removes the given tokens a single time, decrementing the and, + /// eventually, the . + /// + /// The token. + /// The additional tokens. + /// + /// token + /// or + /// additionalTokens + /// + /// + public void PurgeToken(IToken token, params IToken[] additionalTokens) + { + if (ReferenceEquals(token, null)) throw new ArgumentNullException("token"); + if (ReferenceEquals(additionalTokens, null)) throw new ArgumentNullException("additionalTokens"); + + PurgeTokenInternal(token); + PurgeToken(additionalTokens); + } + + /// + /// Removes the given tokens a single time, decrementing the and, + /// eventually, the . + /// + /// The tokens. + /// tokens + /// + public void PurgeToken(IEnumerable tokens) + { + if (ReferenceEquals(tokens, null)) throw new ArgumentNullException("tokens"); + + foreach (var token in tokens) + { + PurgeTokenInternal(token); + } + } + + /// + /// Purges the tokens fulfilling the given predicate. + /// + /// The predicate. + public void PurgeWhere(Predicate predicate) + { + var candidateForPurge = from pair in _tokenCount + let tokenCount = new TokenCount(pair.Key, pair.Value) + where predicate(tokenCount) + select pair.Key; + PurgeToken(candidateForPurge); + } + + /// + /// Removes the single token internally. + /// + /// The token. + private void RemoveSingleTokenInternal(IToken token) + { + long count; + while (_tokenCount.TryGetValue(token, out count)) + { + var newValue = count - 1; + var collectionUpdated = _tokenCount.TryUpdate(token, newValue: newValue, comparisonValue: count); + if (!collectionUpdated) continue; + Interlocked.Decrement(ref _setSize); + + if (newValue == 0) + { + // explicit removal if the count is zero + var collection = _tokenCount as ICollection>; + collection.Remove(new KeyValuePair(token, 0)); + } + + break; + } + } + + /// + /// Purges a single token internally. + /// + /// The token. + private void PurgeTokenInternal(IToken token) + { + long count; + if (!_tokenCount.TryRemove(token, out count)) return; + + // decrement 'count' times + // TODO: use Interlocked.CompareExchange + for (int i = 0; i < count; ++i) + { + Interlocked.Decrement(ref _setSize); + } + } + + /// + /// Factory to initialize the value in for the given . + /// + /// The token. + /// System.Int64. + private static long AddFirsIToken(IToken token) + { + return 1; + } + + /// + /// Factory to increment the value in for the given . + /// + /// The token. + /// The number of tokens. + /// System.Int64. + private static long IncremenITokenCount(IToken token, long count) + { + return count + 1; + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// A that can be used to iterate through the collection. + public IEnumerator GetEnumerator() + { + return _tokenCount.Select(token => new TokenCount(token.Key, token.Value)).GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/BotSharp.Algorithm/Bayesian/EmptyDataSet.cs b/BotSharp.Algorithm/Bayesian/EmptyDataSet.cs new file mode 100644 index 00000000..a9a2690d --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/EmptyDataSet.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections; +using System.Collections.Generic; + + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Class EmptyDataSet. This class cannot be inherited. + /// + internal sealed class EmptyDataSet : IDataSet + { + /// + /// Returns an enumerator that iterates through the collection. + /// + /// A that can be used to iterate through the collection. + public IEnumerator GetEnumerator() + { + yield break; + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Gets the token count. + /// + /// The token count. + public long TokenCount { get { return 0; } } + + /// + /// Gets the size of the set. + /// + /// The size of the set. + public long SetSize { get { return 0; } } + + /// + /// Gets the class. + /// + /// The class. + public IClass Class { get; private set; } + + /// + /// Gets the with the specified token. + /// + /// The token. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// TokenInformation<IToken>. + public TokenInformation this[IToken token, double alpha = 0D] + { + get { return new TokenInformation(token, 0L, 0D); } + } + + /// + /// Initializes a new instance of the class. + /// + /// The class. + /// class + public EmptyDataSet(IClass @class) + { + if (ReferenceEquals(null, @class)) throw new ArgumentNullException("class"); + Class = @class; + } + + /// + /// Gets the count. + /// + /// The token. + /// System.Int64. + public long GetCount(IToken token) + { + return 0L; + } + + /// + /// Gets the percentage. + /// + /// The token. + /// The alpha. + /// System.Double. + /// + public double GetPercentage(IToken token, double alpha = 0) + { + return 0D; + } + + /// + /// Adds the token. + /// + /// The token. + /// The additional tokens. + /// Adding data to the empty data set is not allowed. + public void AddToken(IToken token, params IToken[] additionalTokens) + { + throw new InvalidOperationException("Adding data to the empty data set is not allowed."); + } + + /// + /// Adds the token. + /// + /// The tokens. + /// Adding data to the empty data set is not allowed. + public void AddToken(IEnumerable tokens) + { + throw new InvalidOperationException("Adding data to the empty data set is not allowed."); + } + + /// + /// Removes the token once. + /// + /// The token. + /// The additional tokens. + public void RemoveTokenOnce(IToken token, params IToken[] additionalTokens) + { + } + + /// + /// Removes the token once. + /// + /// The tokens. + public void RemoveTokenOnce(IEnumerable tokens) + { + } + + /// + /// Purges the token. + /// + /// The token. + /// The additional tokens. + public void PurgeToken(IToken token, params IToken[] additionalTokens) + { + } + + /// + /// Purges the token. + /// + /// The tokens. + public void PurgeToken(IEnumerable tokens) + { + } + } +} diff --git a/BotSharp.Algorithm/Bayesian/IClass.cs b/BotSharp.Algorithm/Bayesian/IClass.cs new file mode 100644 index 00000000..e0d9c0fd --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/IClass.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel; + + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Interface IClass + /// + public interface IClass : IEquatable + { + /// + /// Gets the name. + /// + /// The name. + string Name { get; } + + /// + /// Gets or sets the class' base probability. + /// + /// The probability. + [DefaultValue(1)] + double Probability { get; set; } + } +} diff --git a/BotSharp.Algorithm/Bayesian/IClassifier.cs b/BotSharp.Algorithm/Bayesian/IClassifier.cs new file mode 100644 index 00000000..f1a2fa79 --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/IClassifier.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Interface IClassifier + /// + public interface IClassifier + { + /// + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// + /// Laplace smoothing is required in the context or rare (i.e. untrained) tokens or tokens + /// that do not appear in some classes. With smoothing disabled, these tokens result + /// in a zero probability for the whole class. To counter that, a positive ("alpha") + /// value for smoothing can be set. + /// + /// + double SmoothingAlpha { get; set; } + + /// + /// Calculates the probability of having the + /// given the occurrence of the . + /// + /// The class under test. + /// The token. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied, setting to defaults to the values set in . + /// System.Double. + double CalculateProbability(IClass classUnderTest, IToken token, double? alpha = null); + + /// + /// Calculates the probability of having the + /// + /// given the occurrence of the + /// . + /// + /// The token. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied, setting to defaults to the values set in . + /// System.Double. + IEnumerable CalculateProbabilities(IToken token, double? alpha = null); + + /// + /// Calculates the probability of having the + /// + /// given the occurrence of the + /// . + /// + /// The tokens. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied, setting to defaults to the values set in . + /// System.Double. + IEnumerable CalculateProbabilities(ICollection tokens, double? alpha = null); + } +} \ No newline at end of file diff --git a/BotSharp.Algorithm/Bayesian/IDataSet.cs b/BotSharp.Algorithm/Bayesian/IDataSet.cs new file mode 100644 index 00000000..f9118c50 --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/IDataSet.cs @@ -0,0 +1,9 @@ +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Interface IDataSet + /// + public interface IDataSet : IDataSetAccessor, ITokenRegistration + { + } +} \ No newline at end of file diff --git a/BotSharp.Algorithm/Bayesian/IDataSetAccessor.cs b/BotSharp.Algorithm/Bayesian/IDataSetAccessor.cs new file mode 100644 index 00000000..3278bb60 --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/IDataSetAccessor.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; + + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Interface IDataSetAccessor + /// + public interface IDataSetAccessor : IEnumerable + { + /// + /// Gets the number of distinct tokens, + /// i.e. every token counted at exactly once. + /// + /// The token count. + /// + long TokenCount { get; } + + /// + /// Gets the size of the set. + /// + /// The size of the set. + /// + long SetSize { get; } + + /// + /// Gets the class. + /// + /// The class. + IClass Class { get; } + + /// + /// Gets the with the specified token. + /// + /// The token. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// TokenInformation<IToken>. + /// token + TokenInformation this[IToken token, double alpha] { get; } + + /// + /// Gets the number of occurrences of the given token. + /// + /// The token. + /// System.Int64. + /// token + /// + long GetCount(IToken token); + + /// + /// Gets the approximated percentage of the given + /// in this data set + /// by determining its occurrence count over the whole population. + /// + /// The token. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// System.Double. + /// token + /// + double GetPercentage(IToken token, double alpha); + } +} \ No newline at end of file diff --git a/BotSharp.Algorithm/Bayesian/IToken.cs b/BotSharp.Algorithm/Bayesian/IToken.cs new file mode 100644 index 00000000..165191ab --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/IToken.cs @@ -0,0 +1,11 @@ +using System; + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Interface IToken + /// + public interface IToken : IEquatable + { + } +} diff --git a/BotSharp.Algorithm/Bayesian/ITokenRegistration.cs b/BotSharp.Algorithm/Bayesian/ITokenRegistration.cs new file mode 100644 index 00000000..bf2e53d9 --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/ITokenRegistration.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; + + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Interface ITokenRegistration + /// + public interface ITokenRegistration + { + /// + /// Adds the given tokens a single time, incrementing the + /// and, at the first addition, the . + /// + /// The token. + /// The additional tokens. + /// + /// token + /// or + /// additionalTokens + /// + void AddToken(IToken token, params IToken[] additionalTokens); + + /// + /// Adds the given tokens a single time, incrementing the + /// and, at the first addition, the . + /// + /// The tokens. + /// tokens + void AddToken(IEnumerable tokens); + + /// + /// Removes the given tokens a single time, decrementing the and, + /// eventually, the . + /// + /// The token. + /// The additional tokens. + /// + /// token + /// or + /// additionalTokens + /// + /// + void RemoveTokenOnce(IToken token, params IToken[] additionalTokens); + + /// + /// Removes the given tokens a single time, decrementing the and, + /// eventually, the . + /// + /// The tokens. + /// tokens + /// + void RemoveTokenOnce(IEnumerable tokens); + + /// + /// Removes the given tokens a single time, decrementing the and, + /// eventually, the . + /// + /// The token. + /// The additional tokens. + /// + /// token + /// or + /// additionalTokens + /// + /// + void PurgeToken(IToken token, params IToken[] additionalTokens); + + /// + /// Removes the given tokens a single time, decrementing the and, + /// eventually, the . + /// + /// The tokens. + /// tokens + /// + void PurgeToken(IEnumerable tokens); + } +} \ No newline at end of file diff --git a/BotSharp.Algorithm/Bayesian/ITrainingSet.cs b/BotSharp.Algorithm/Bayesian/ITrainingSet.cs new file mode 100644 index 00000000..dd6b0353 --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/ITrainingSet.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Interface ITrainingSet + /// + public interface ITrainingSet : ITrainingSetAccessor + { + /// + /// Adds the specified data set. + /// + /// The data set. + /// The additional data sets. + /// dataSet + /// A data set for a given class was already registered. + void Add(IDataSet dataSet, params IDataSet[] additionalDataSets); + + /// + /// Adds the specified data sets. + /// + /// The data sets. + /// dataSets + /// A data set for a given class was already registered. + void Add(IEnumerable dataSets); + } +} \ No newline at end of file diff --git a/BotSharp.Algorithm/Bayesian/ITrainingSetAccessor.cs b/BotSharp.Algorithm/Bayesian/ITrainingSetAccessor.cs new file mode 100644 index 00000000..3eebfe42 --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/ITrainingSetAccessor.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Interface ITrainingSetAccerssor + /// + public interface ITrainingSetAccessor : IEnumerable + { + /// + /// Gets the with the specified class. + /// + /// The class. + /// IDataSet<TClass, TToken>. + /// No data set was registered for the given class;class + IDataSet this[IClass @class] { get; } + } +} \ No newline at end of file diff --git a/BotSharp.Algorithm/Bayesian/LinqExtensions.cs b/BotSharp.Algorithm/Bayesian/LinqExtensions.cs new file mode 100644 index 00000000..d97440ac --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/LinqExtensions.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; + + +namespace BotSharp.Algorithm.Bayesian +{ + internal static class LinqExtensions + { + /// + /// Converts an enumerable to a collection + /// + /// + /// The enumerable. + /// ICollection<T>. + public static ICollection ToCollection(this IEnumerable enumerable) + { + var type = enumerable.GetType(); + if (type.IsGenericCollectionType()) return (ICollection)enumerable; + + var collection = new Collection(); + foreach (var t in enumerable) + { + collection.Add(t); + } + return collection; + } + + /// + /// Forces evaluation of the enumerable + /// + /// + /// The enumerable. + public static void Run(this IEnumerable enumerable) + { + var type = enumerable.GetType(); + if (type.IsGenericCollectionType()) return; + + foreach (var item in enumerable) + { + } + } + + /// + /// The cache for + /// + private static readonly ConcurrentDictionary IsGenericCollectionTypeCache = new ConcurrentDictionary(); + + /// + /// Determines whether the specified type is a (generic) collection. + /// + /// The type. + /// true if the specified type is collection; otherwise, false. + public static bool IsGenericCollectionType(this Type type) + { + return IsGenericCollectionTypeCache.GetOrAdd(type, t => type.GetInterfaces() + .Any(ti => ti.IsGenericType + && + ti.GetGenericTypeDefinition() == + typeof (ICollection<>))); + + } + } +} diff --git a/BotSharp.Algorithm/Bayesian/NaiveClassifier.cs b/BotSharp.Algorithm/Bayesian/NaiveClassifier.cs new file mode 100644 index 00000000..f045503d --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/NaiveClassifier.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; + + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Class NaiveClassifier. This class cannot be inherited. + /// + /// Assumes that all token occurrences are statistically independent. + /// + /// + public sealed class NaiveClassifier : IClassifier + { + /// + /// The training sets + /// + private readonly ITrainingSetAccessor _trainingSets; + + /// + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// + private double _smoothingAlpha = 0.01D; + + /// + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// + [DefaultValue(0.01D)] + public double SmoothingAlpha + { + get { return _smoothingAlpha; } + set + { + if (value <= 0) throw new ArgumentOutOfRangeException("value", value, "Value must be greater than zero."); + _smoothingAlpha = value; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The training sets. + /// trainingSets + public NaiveClassifier(ITrainingSetAccessor trainingSets) + { + if (ReferenceEquals(trainingSets, null)) throw new ArgumentNullException("trainingSets"); + _trainingSets = trainingSets; + } + + /// + /// Calculates the probability of having the + /// given the occurrence of the . + /// + /// The class under test. + /// The token. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// System.Double. + public double CalculateProbability(IClass classUnderTest, IToken token, double? alpha = null) + { + var smoothingAlpha = alpha ?? _smoothingAlpha; + + ICollection remainingSets; + var setForClassUnderTest = SplitDataSets(classUnderTest, out remainingSets); + + // calculate the token's probability in the class under test + var percentageInClassUnderTest = setForClassUnderTest.GetPercentage(token, smoothingAlpha); + var probabilityInClassUnderTest = percentageInClassUnderTest * classUnderTest.Probability; + + // calculate the token's probabilities for the remaining classes + double sumOfRemainingProbabilites; + CalculateTokenProbabilityGivenClass(token, remainingSets, out sumOfRemainingProbabilites, smoothingAlpha).Run(); + + // calculate total probability + var totalProbability = probabilityInClassUnderTest + sumOfRemainingProbabilites; + + // calculate the class' probability given the token + var probabilityForClass = probabilityInClassUnderTest/totalProbability; + + // correct for rare words + return probabilityForClass; + } + + /// + /// Calculates the probability of having the + /// + /// given the occurrence of the + /// . + /// + /// The token. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// System.Double. + public IEnumerable CalculateProbabilities(IToken token, double? alpha = null) + { + var smoothingAlpha = alpha ?? _smoothingAlpha; + + // calculate the token's probabilities for all classes + double totalProbability; + var probabilities = CalculateTokenProbabilityGivenClass(token, _trainingSets, out totalProbability, smoothingAlpha); + + // apply Bayes theorem + var inverseOfTotalProbability = 1.0D/totalProbability; + return from cp in probabilities + let conditionalProbability = cp.Probability * inverseOfTotalProbability + select new ConditionalProbability(cp.Class, cp.Token, conditionalProbability, cp.Occurrence); + } + + /// + /// Calculates the probability of having the + /// + /// given the occurrence of the + /// . + /// + /// The tokens. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// System.Double. + public IEnumerable CalculateProbabilities(ICollection tokens, double? alpha = null) + { + var smoothingAlpha = alpha ?? _smoothingAlpha; + + var cpgs = tokens + .SelectMany(token => CalculateProbabilities(token, smoothingAlpha)) + .GroupBy(cp => cp.Class) + .ToCollection(); + + return from @group in cpgs + let cps = @group.ToCollection() + let eta = cps.Select(cp => cp.Probability) + .Sum(p => Math.Log(1 - p) - Math.Log(p)) + let probability = 1/(1 + Math.Exp(eta)) + select new CombinedConditionalProbability(@group.Key, probability, cps); + } + + /// + /// Calculates the token probabilities given a class. + /// + /// The token. + /// The sets. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// IEnumerable<ConditionalProbability<IClass, IToken>>. + private IEnumerable CalculateTokenProbabilityGivenClass(IToken token, IEnumerable sets, double alpha) + { + return from set in sets + let @class = set.Class + let classProbability = @class.Probability + let percentageInClass = set.GetPercentage(token, alpha) + let countInClass = set.GetCount(token) + let probabilityInClass = percentageInClass*classProbability + select new ConditionalProbability(@class, token, probabilityInClass, countInClass); + } + + /// + /// Calculates the token probabilities given a class. + /// + /// The token. + /// The sets. + /// Additive smoothing parameter. If set to zero, no Laplace smoothing will be applied. + /// The total probability for the given classes. + /// IEnumerable<ConditionalProbability<IClass, IToken>>. + private IEnumerable CalculateTokenProbabilityGivenClass(IToken token, IEnumerable sets, out double totalProbability, double alpha) + { + var probabilities = CalculateTokenProbabilityGivenClass(token, sets, alpha).ToCollection(); + totalProbability = probabilities.Sum(p => p.Probability); + return probabilities; + } + + /// + /// Splits the data sets. + /// + /// The class under test. + /// The remaining sets. + /// IDataSet<IClass, IToken>. + private IDataSetAccessor SplitDataSets(IClass classUnderTest, out ICollection remainingSets) + { + IDataSet setForClassUnderTest = null; + remainingSets = new Collection(); + + // split data sets by selected class and other classes + foreach (var trainingSet in _trainingSets) + { + // select the set for the class under test + if (trainingSet.Class.Equals(classUnderTest)) + { + Debug.Assert(setForClassUnderTest == null, + "The class under test must not have multiple sets registered in the DataSet"); + setForClassUnderTest = trainingSet; + continue; + } + + // select remaining sets + remainingSets.Add(trainingSet); + } + + // return the found set or an empty set + return setForClassUnderTest ?? new EmptyDataSet(classUnderTest); + } + } +} diff --git a/BotSharp.Algorithm/Bayesian/StringClass.cs b/BotSharp.Algorithm/Bayesian/StringClass.cs new file mode 100644 index 00000000..7f0b9fa1 --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/StringClass.cs @@ -0,0 +1,84 @@ +using System; + + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Class StringClass. This class cannot be inherited. + /// + public sealed class StringClass : ClassBase + { + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// The probability. + /// name + /// + /// probability;Class base probability must be greater than or equal to zero. + /// or + /// probability;Class base probability must be less than or equal to one. + /// + public StringClass(string name, double probability) + : base(name, probability) + { + } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with the current . + /// if the specified is equal to this instance; otherwise, . + public override bool Equals(IClass other) + { + var otherAsObject = (object) other; + return Equals(otherAsObject); + } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with the current . + /// if the specified is equal to this instance; otherwise, . + private bool Equals(StringClass other) + { + if (ReferenceEquals(other, null)) return false; + if (ReferenceEquals(other, this)) return true; + return String.Equals(Name, other.Name) + && Math.Abs(Probability - other.Probability) < 0.0001D; + } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with the current . + /// if the specified is equal to this instance; otherwise, . + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is StringClass && Equals((StringClass) obj); + } + + /// + /// Returns a hash code for this instance. + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + public override int GetHashCode() + { + var hash = 27; + hash = (13 * hash) + Name.GetHashCode(); + hash = (13 * hash) + Probability.GetHashCode(); + return hash; + } + + /// + /// Returns a that represents this instance. + /// + /// A that represents this instance. + public override string ToString() + { + return Name; + } + } +} diff --git a/BotSharp.Algorithm/Bayesian/StringToken.cs b/BotSharp.Algorithm/Bayesian/StringToken.cs new file mode 100644 index 00000000..6f19633c --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/StringToken.cs @@ -0,0 +1,81 @@ +using System; +using System.Diagnostics; + + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Class StringToken. This class cannot be inherited. + /// + [DebuggerDisplay("{Value}")] + public sealed class StringToken : IToken + { + /// + /// Gets the value. + /// + /// The value. + public string Value { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The value. + public StringToken(string value) + { + if (ReferenceEquals(value, null)) throw new ArgumentNullException("value"); + Value = value; + } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with the current . + /// if the specified is equal to this instance; otherwise, . + private bool Equals(StringToken other) + { + return string.Equals(Value, other.Value); + } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with the current . + /// if the specified is equal to this instance; otherwise, . + bool IEquatable.Equals(IToken other) + { + var otherAsObject = (object)other; + return Equals(otherAsObject); + } + + /// + /// Determines whether the specified is equal to this instance. + /// + /// The to compare with the current . + /// if the specified is equal to this instance; otherwise, . + public override bool Equals(object obj) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is StringToken && Equals((StringToken) obj); + } + + /// + /// Returns a hash code for this instance. + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + /// + /// Returns a that represents this instance. + /// + /// A that represents this instance. + public override string ToString() + { + return Value; + } + } +} diff --git a/BotSharp.Algorithm/Bayesian/TokenCount.cs b/BotSharp.Algorithm/Bayesian/TokenCount.cs new file mode 100644 index 00000000..bc72d351 --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/TokenCount.cs @@ -0,0 +1,37 @@ +using System; + + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Struct TokenCount + /// + public struct TokenCount + { + /// + /// The token + /// + public readonly IToken Token; + + /// + /// The number of occurrences + /// + public readonly long Count; + + /// + /// Initializes a new instance of the struct. + /// + /// The token. + /// The count. + /// token + /// count;Count must be positive or zero. + public TokenCount(IToken token, long count) + { + if (ReferenceEquals(token, null)) throw new ArgumentNullException("token"); + if (count < 0) throw new ArgumentOutOfRangeException("count", count, "Count must be positive or zero."); + + Token = token; + Count = count; + } + } +} diff --git a/BotSharp.Algorithm/Bayesian/TokenInformation.cs b/BotSharp.Algorithm/Bayesian/TokenInformation.cs new file mode 100644 index 00000000..44fbad95 --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/TokenInformation.cs @@ -0,0 +1,50 @@ +using System; + + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Struct TokenInformation + /// + public struct TokenInformation + where TToken: IToken + { + /// + /// The token + /// + public readonly TToken Token; + + /// + /// The count in the class + /// + public long Count; + + /// + /// The occurrence percentage of the token in the class. + /// + public double Percentage; + + /// + /// Initializes a new instance of the struct. + /// + /// The token. + /// The count. + /// The percentage. + /// token + /// + /// count;Count must be positive or zero + /// or + /// percentage;Percentage must be positive or zero + /// + public TokenInformation(TToken token, long count, double percentage) + { + if (ReferenceEquals(token, null)) throw new ArgumentNullException("token"); + if (count < 0) throw new ArgumentOutOfRangeException("count", count, "Count must be positive or zero"); + if (percentage < 0) throw new ArgumentOutOfRangeException("percentage", percentage, "Percentage must be positive or zero"); + + Token = token; + Count = count; + Percentage = percentage; + } + } +} diff --git a/BotSharp.Algorithm/Bayesian/TrainingSet.cs b/BotSharp.Algorithm/Bayesian/TrainingSet.cs new file mode 100644 index 00000000..8d9984fc --- /dev/null +++ b/BotSharp.Algorithm/Bayesian/TrainingSet.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + + +namespace BotSharp.Algorithm.Bayesian +{ + /// + /// Class TrainingSet. This class cannot be inherited. + /// + public sealed class TrainingSet : ITrainingSet + { + /// + /// The data sets + /// + private readonly ConcurrentDictionary _dataSets = new ConcurrentDictionary(); + + /// + /// Initializes a new instance of the class. + /// + public TrainingSet() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The data sets. + public TrainingSet(IEnumerable dataSets) + { + Add(dataSets); + } + + /// + /// Initializes a new instance of the class. + /// + /// The data set. + /// The additional data sets. + public TrainingSet(IDataSet dataSet, params IDataSet[] additionalDataSets) + { + Add(dataSet, additionalDataSets); + } + + /// + /// Gets the with the specified class. + /// + /// The class. + /// IDataSet<IClass, IToken>. + /// No data set was registered for the given class;class + public IDataSet this[IClass @class] + { + get + { + IDataSet set; + if (_dataSets.TryGetValue(@class, out set)) return set; + throw new ArgumentException("No data set was registered for the given class", "class"); + } + } + + /// + /// Adds the specified data set. + /// + /// The data set. + /// The additional data sets. + /// dataSet + /// A data set for a given class was already registered. + public void Add(IDataSet dataSet, params IDataSet[] additionalDataSets) + { + if (ReferenceEquals(dataSet, null)) throw new ArgumentNullException("dataSet"); + + try + { + AddInternal(dataSet); + } + catch (ArgumentException e) + { + throw new ArgumentException("A data set for a given class was already registered.", e); + } + + // may throw, that's anticipated + Add(additionalDataSets); + } + + /// + /// Adds the specified data sets. + /// + /// The data sets. + /// dataSets + /// A data set for a given class was already registered. + public void Add(IEnumerable dataSets) + { + if (ReferenceEquals(dataSets, null)) throw new ArgumentNullException("dataSets"); + + try + { + foreach (var dataSet in dataSets) + { + AddInternal(dataSet); + } + } + catch (ArgumentException e) + { + throw new ArgumentException("A data set for a given class was already registered.", e); + } + } + + + /// + /// Adds the data set internally. + /// + /// The data set. + /// Data set for the given class was already registered. + private void AddInternal(IDataSet dataSet) + { + if (!_dataSets.TryAdd(dataSet.Class, dataSet)) + { + throw new ArgumentException("Data set for the given class was already registered."); + } + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// A that can be used to iterate through the collection. + public IEnumerator GetEnumerator() + { + return _dataSets.Select(dataSet => dataSet.Value).GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +}