Monday, October 21, 2013

C#: USD Wallet - Main Class


#region License
// This notice must be kept visible in the source.
// This section of source code belongs to Protiguous@Protiguous.Info.
// Royalties must be paid via bitcoin @ 1PRoT78h5EuPgWECtgFYeVRhUb2tQXskXL
// Usage of the source code or compiled binaries is AS-IS.
// "AI/Wallet.cs" was last cleaned on 2013/10/21.
#endregion
 
namespace AI.Measurement.Currency.USD {
    using System;
    using System.Collections;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Runtime.Serialization;
    using System.Threading.Tasks.Dataflow;
    using Annotations;
    using Maths;
    using NUnit.Framework;
    using Threading;
 
    /// <summary>
    ///     My first go at a thread-safe Wallet class for US dollars and coins.
    ///     It's more pseudocode for learning than for production..
    ///     Use at your own risk, as its thoroughly untested. :) 
    ///     Any tips or ideas? Any dos or dont's? Email me!
    /// </summary>
    [DataContract( IsReference = true )]
    [DebuggerDisplay( "{Formatted,nq}" )]
    public class Wallet : IEnumerable<KeyValuePair<IDenomination, UInt64>> {
 
        [DataMember]
        [NotNull]
        public readonly Statistics Statistics = new Statistics();
 
        /// <summary>
        ///     Count of each <see cref="IBankNote" />.
        /// </summary>
        [NotNull]
        private readonly ConcurrentDictionary<IBankNote, UInt64> _bankNotes = new ConcurrentDictionary<IBankNote, UInt64>();
 
        /// <summary>
        ///     Count of each <see cref="ICoin" />.
        /// </summary>
        [NotNull]
        private readonly ConcurrentDictionary<ICoin, UInt64> _coins = new ConcurrentDictionary<ICoin, UInt64>();
 
        public IEnumerable<KeyValuePair<ICoin, UInt64>> CoinsGrouped {
            [NotNull]
            get {
                Assert.NotNull( this._coins );
                return this._coins;
            }
        }
 
        [UsedImplicitly]
        public String Formatted {
            get {
                var total = this.Total.ToString( "C4" );
                Assert.NotNull( this._bankNotes );
                var notes = this._bankNotes.Aggregate( 0UL, ( current, pair ) => current + pair.Value );
 
                Assert.NotNull( this._coins );
                var coins = this._coins.Aggregate( 0UL, ( current, pair ) => current + pair.Value );
                return String.Format( "{0} in {1:N0} notes and {2:N0} coins.", total, notes, coins );
            }
        }
 
        /// <summary>
        ///     Return an expanded list of the <see cref="Notes" /> and <see cref="Coins" /> in this <see cref="Wallet" />.
        /// </summary>
        public IEnumerable<IDenomination> NotesAndCoins {
            [NotNull]
            get { return this.Coins.Concat<IDenomination>( this.Notes ); }
        }
 
        public IEnumerable<KeyValuePair<IBankNote, UInt64>> NotesGrouped {
            [NotNull]
            get { return this._bankNotes; }
        }
 
        /// <summary>
        ///     Return each <see cref="ICoin" /> in this <see cref="Wallet" />.
        /// </summary>
        public IEnumerable<ICoin> Coins {
            [NotNull]
            get { return this._coins.SelectMany( pair => 1.To( pair.Value ), ( pair, valuePair ) => pair.Key ); }
        }
 
        /// <summary>
        ///     Return the count of each type of <see cref="Notes" /> and <see cref="Coins" />.
        /// </summary>
        public IEnumerable<KeyValuePair<IDenomination, UInt64>> Groups {
            [NotNull]
            get {
                return this._bankNotes.Cast<KeyValuePair<IDenomination, ulong>>()
                           .Concat( this._coins.Cast<KeyValuePair<IDenomination, ulong>>() );
            }
        }
 
        public Guid ID { get; private set; }
 
        /// <summary>
        ///     Return each <see cref="IBankNote" /> in this <see cref="Wallet" />.
        /// </summary>
        public IEnumerable<IBankNote> Notes { get { return this._bankNotes.SelectMany( pair => 1.To( pair.Value ), ( pair, valuePair ) => pair.Key ); } }
 
        /// <summary>
        ///     Return the total amount of money contained in this <see cref="Wallet" />.
        /// </summary>
        public Decimal Total {
            get {
                var total = this._coins.Aggregate( Decimal.Zero, ( current, pair ) => current + pair.Key.FaceValue * pair.Value );
                total += this._bankNotes.Aggregate( Decimal.Zero, ( current, pair ) => current + pair.Key.FaceValue * pair.Value );
                return total;
            }
        }
 
        private ActionBlock<TransactionMessage> Actor { get; set; }
 
        private Wallet( Guid id ) {
            this.ID = id;
            this.Statistics.Reset();
            this.Actor = new ActionBlock<TransactionMessage>( message => {
                switch ( message.TransactionType ) {
                    case TransactionType.Deposit:
                        Extensions.Deposit( this, message );
                        break;
                    case TransactionType.Withdraw:
                        this.TryWithdraw( message );
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }, Blocks.ManyProducers.ConsumeSerial );
        }
 
        private Boolean TryWithdraw( TransactionMessage message ) {
            var asBankNote = message.Denomination as IBankNote;
            if ( null != asBankNote ) {
                return this.TryWithdraw( asBankNote, message.Quantity );
            }
 
            var asCoin = message.Denomination as ICoin;
            if ( null != asCoin ) {
                return this.TryWithdraw( asCoin, message.Quantity );
            }
 
            throw new NotImplementedException( String.Format( "Unknown denomination {0}", message.Denomination ) );
        }
 
        /// <summary>
        ///     Attempt to <see cref="TryWithdraw(IBankNote,ulong)" /> one or more <see cref="IBankNote" /> from this
        ///     <see cref="Wallet" />.
        /// </summary>
        /// <param name="bankNote"></param>
        /// <param name="quantity"></param>
        /// <returns></returns>
        /// <remarks>Locks the wallet.</remarks>
        public Boolean TryWithdraw( [CanBeNull] IBankNote bankNote, UInt64 quantity ) {
            if ( bankNote == null ) {
                return false;
            }
            if ( quantity <= 0 ) {
                return false;
            }
            lock ( this._bankNotes ) {
                if ( !this._bankNotes.ContainsKey( bankNote ) || this._bankNotes[ bankNote ] < quantity ) {
                    return false; //no bills to withdraw!
                }
                this._bankNotes[ bankNote ] -= quantity;
                return true;
            }
        }
 
        /// <summary>
        ///     Attempt to <see cref="TryWithdraw(ICoin,ulong)" /> one or more <see cref="ICoin" /> from this <see cref="Wallet" />
        ///     .
        /// </summary>
        /// <param name="coin"></param>
        /// <param name="quantity"></param>
        /// <returns></returns>
        /// <remarks>Locks the wallet.</remarks>
        public Boolean TryWithdraw( [NotNull] ICoin coin, UInt64 quantity ) {
            if ( coin == null ) {
                throw new ArgumentNullException( "coin" );
            }
            if ( quantity <= 0 ) {
                return false;
            }
            lock ( this._coins ) {
                if ( !this._coins.ContainsKey( coin ) || this._coins[ coin ] < quantity ) {
                    return false; //no coins to withdraw!
                }
                this._coins[ coin ] -= quantity;
                return true;
            }
        }
 
        public IEnumerator<KeyValuePair<IDenomination, UInt64>> GetEnumerator() {
            return this.Groups.GetEnumerator();
        }
 
        IEnumerator IEnumerable.GetEnumerator() {
            return this.GetEnumerator();
        }
 
        /// <summary>
        ///     Create an empty wallet with a new random id.
        /// </summary>
        /// <returns></returns>
        [NotNull]
        public static Wallet Create() {
            return new Wallet( id: Guid.NewGuid() );
        }
 
        /// <summary>
        ///     Create an empty wallet with the given <paramref name="id" />.
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [NotNull]
        public static Wallet Create( Guid id ) {
            return new Wallet( id: id );
        }
 
        public Boolean Contains( [NotNull] IBankNote bankNote ) {
            if ( bankNote == null ) {
                throw new ArgumentNullException( "bankNote" );
            }
            return this._bankNotes.ContainsKey( bankNote );
        }
 
        public Boolean Contains( [NotNull] ICoin coin ) {
            if ( coin == null ) {
                throw new ArgumentNullException( "coin" );
            }
            return this._coins.ContainsKey( coin );
        }
 
        public UInt64 Count( [NotNull] IBankNote bankNote ) {
            if ( bankNote == null ) {
                throw new ArgumentNullException( "bankNote" );
            }
            ulong result;
            return this._bankNotes.TryGetValue( bankNote, out result ) ? result : UInt64.MinValue;
        }
 
        public UInt64 Count( [NotNull] ICoin coin ) {
            if ( coin == null ) {
                throw new ArgumentNullException( "coin" );
            }
            ulong result;
            return this._coins.TryGetValue( coin, out result ) ? result : UInt64.MinValue;
        }
 
        public Boolean TryWithdraw( [CanBeNull] IDenomination denomination, UInt64 quantity ) {
            var asBankNote = denomination as IBankNote;
            if ( null != asBankNote ) {
                return this.TryWithdraw( asBankNote, quantity );
            }
 
            var asCoin = denomination as ICoin;
            if ( null != asCoin ) {
                return this.TryWithdraw( asCoin, quantity );
            }
 
            throw new NotImplementedException( String.Format( "Unknown denomination {0}", denomination ) );
        }
 
        /// <summary>
        ///     Deposit one or more <paramref name="denomination" /> into this <see cref="Wallet" />.
        /// </summary>
        /// <param name="denomination"></param>
        /// <param name="quantity"></param>
        /// <param name="id"></param>
        /// <returns></returns>
        /// <remarks>Locks the wallet.</remarks>
        public Boolean Deposit( [NotNull] IDenomination denomination, UInt64 quantity, Guid? id = null ) {
            if ( denomination == null ) {
                throw new ArgumentNullException( "denomination" );
            }
            if ( quantity <= 0 ) {
                return false;
            }
 
            return this.Actor.Post( new TransactionMessage {
                Date = DateTime.Now,
                Denomination = denomination,
                ID = id ?? Guid.NewGuid(),
                Quantity = quantity,
                TransactionType = TransactionType.Deposit
            } );
        }
 
        private ulong Deposit( ICoin asCoin, UInt64 quantity ) {
            if ( null == asCoin ) {
                return 0;
            }
            try {
                lock ( this._coins ) {
                    UInt64 newQuantity = 0;
                    if ( !this._coins.ContainsKey( asCoin ) ) {
                        if ( this._coins.TryAdd( asCoin, quantity ) ) {
                            newQuantity = quantity;
                        }
                    }
                    else {
                        newQuantity = this._coins[ asCoin ] += quantity;
                    }
                    return newQuantity;
                }
            }
            finally {
                this.Statistics.AllTimeDeposited += asCoin.FaceValue * quantity;
            }
        }
 
        private ulong Deposit( IBankNote bankNote, UInt64 quantity ) {
            if ( null == bankNote ) {
                return 0;
            }
            try {
                lock ( this._bankNotes ) {
                    UInt64 newQuantity = 0;
                    if ( !this._bankNotes.ContainsKey( bankNote ) ) {
                        if ( this._bankNotes.TryAdd( bankNote, quantity ) ) {
                            newQuantity = quantity;
                        }
                    }
                    else {
                        newQuantity = this._bankNotes[ bankNote ] += quantity;
                    }
                    return newQuantity;
                }
            }
            finally {
                this.Statistics.AllTimeDeposited += bankNote.FaceValue * quantity;
            }
        }
    }
}

No comments: