c#コンソールアプリで学ぶ、自動販売機の釣り銭機能

2022年12月16日

自動販売機や券売機の釣り銭機能をシミュレートする実験をしてみましょう
商品の価格管理や入金の管理は含まれていません
出金部分だけに着目して考えてみましょう

機能

ある商品の価格が決まっており、預かり金をいただいた時にお釣りを払い出す機能を考えてみましょう
ちなみに出金機能に特化しており、入金機能は網羅していません
入金機能とは、商品を購入したときに入金されたお金を還元して釣り銭としてストックする機能です

金種を配列で管理

存在する金種ごとに配列を作ります
10000円、5000円、1000円、500円、100円、50円、10円、5円、1円の9種類あるので、要素数は9ですね

コード

シミュレートコードを見てみましょう
簡略化のため、エラー処理は含まれていません

金種ごとの基本となるクラス

釣り銭機に格納されているお金のストックを作りますが、それぞれの金種の情報をクラスとして情報を持つことにします

  • 種別(1000円の場合は、1000)
  • 数量(1000円が3枚であれば3)
namespace VendingMachine
{
    public struct Money
    {
        public Money(int type, int count)
        {
            Type = type;
            Count = count;
        }

        public int Type { get; set; }
        public int Count { get; set; }
    }
}

釣り銭機クラス

釣り銭機クラスを作ります釣り銭機は、金種分の格納エリアがあるとします(stock)

コンストラクタで、初期設投入金額を金種ごとの数量として登録します

namespace VendingMachine
{
    public class VendingMachine
    {
        // 格納金
        Money[] stock;
        // 現在の格納金の値を一時保管(払い出し不良の場合、この値をstockに戻します)
        Money[] tempStock;
        // 釣り銭切れの金種
        List<Money> emptyChanges = new List<Money>();

        public VendingMachine(
            Money yen10000,
            Money yen5000,
            Money yen1000,
            Money yen500,
            Money yen100,
            Money yen50,
            Money yen10,
            Money yen5,
            Money yen1)
        {
            stock = new[] { yen10000, yen5000,/* yen2000,*/ yen1000, yen500, yen100, yen50, yen10, yen5, yen1 };

        }

        public Money[] Stock
        {
            get => stock;
        }

        /// <summary>
        /// 釣り銭の処理
        /// </summary>
        /// <param name="price">価格</param>
        /// <param name="deposit">預かり金</param>
        /// <returns>釣り銭</returns>
        public Money[]? HandlingChange(int price, int deposit)
        {
            tempStock = new Money[stock.Length];

            Array.Copy(stock, tempStock, stock.Length);

            emptyChanges.Clear();

            // 金種ごとの処理
            for (int i = 0; i < stock.Length; i++)
            {
                CalcType(price, ref deposit, i);
            }

            // 釣り銭が足りない
            if (deposit - price != 0)
            {
                stock = tempStock;
                return null;
            }

            var changes = new List<Money>();

            for (int i = 0; i < stock.Length; i++)
            {
                changes.Add(new Money(tempStock[i].Type, tempStock[i].Count - stock[i].Count));
            }

            return changes.ToArray();
        }

        /// <summary>
        /// 釣り銭の種類による払い出し処理
        /// </summary>
        /// <param name="price">価格</param>
        /// <param name="deposit">預かり金</param>
        /// <param name="index">お金の金種</param>
        void CalcType(int price, ref int deposit, int index)
        {
            // 価格が残りの預かり金以上の場合、処理しない
            if (deposit <= price)
            {
                return;
            }

            while (deposit - price >= stock[index].Type)
            {
                // 釣り銭の一部が空の場合
                if (stock[index].Count == 0)
                {
                    emptyChanges.Add(stock[index]);
                    return;
                }

                deposit -= stock[index].Type;

                stock[index].Count--;
            }
        }

        /// <summary>
        /// 釣り銭のセット
        /// </summary>
        /// <param name="type">金種</param>
        /// <param name="count">金種ごとの数</param>
        /// <returns>セットできたかどうか</returns>
        public bool SeCount(int type, int count)
        {
            for (int i = 0; i < stock.Length; i++)
            {
                if (stock[i].Type == type)
                {
                    stock[i].Count = count;

                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// 釣り銭の追加
        /// </summary>
        /// <param name="type">金種</param>
        /// <param name="count">金種ごとの追加数</param>
        /// <returns>追加できたかどうか</returns>
        public bool AddCount(int type, int count)
        {
            for (int i = 0; i < stock.Length; i++)
            {
                if (stock[i].Type == type)
                {
                    stock[i].Count += count;

                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// 金種ごとの数を取得
        /// </summary>
        /// <param name="type">金種</param>
        /// <returns>金種ごとの数</returns>
        public int GetCount(int type)
        {
            return Array.Find(stock, money => money.Type == type).Count;
        }
    }
}

釣り銭計算をコントロールするクラス

払い出しのため、釣り銭機に命令をだします

次のコードが釣り銭機の払出し命令のところですが、商品価格と預かり金を渡すことで払い出す金種と数量を取得できます

Money[] ChangeDispensing = vendingMachine.HandlingChange(price, deposit);
using System.Linq;

namespace VendingMachine
{
    class Program
    {
        public static void Main()
        {
            // 釣り銭機に釣り銭をセット
            var vendingMachine = setVenderMachine();
            // 釣り銭機残金表示
            ShowVenderMachineMoneies(vendingMachine);
            // レジ入力
            var inputValue = InputSellData();
            // 計算
            Calc(vendingMachine, inputValue.price, inputValue.deposit);
            // 釣り銭機残金表示
            ShowVenderMachineMoneies(vendingMachine);

            static (int price, int deposit) InputSellData()
            {
                Console.WriteLine("\nレジの開始\n-------------");
                Console.Write("価格");
                var price = int.Parse(Console.ReadLine());
                Console.Write("預かり金");
                var deposit = int.Parse(Console.ReadLine());
                Console.WriteLine($"商品代金:{price}円, 預かり金:{deposit}円\n");

                return (price, deposit);
            }

            static void Calc(VendingMachine vendingMachine, int price, int deposit)
            {
                var ChangeDispensing = vendingMachine.HandlingChange(price, deposit);

                Console.WriteLine("払い出す金種と数");

                if (ChangeDispensing == null)
                {
                    Console.WriteLine("払い出す釣り銭が足らないので処理されません");
                    return;
                }

                for (int i = 0; i < ChangeDispensing.Length; i++)
                {
                    Console.WriteLine($"{ChangeDispensing[i].Type}円 : {ChangeDispensing[i].Count}");
                }
            }

            static void ShowVenderMachineMoneies(VendingMachine vendingMachine)
            {
                // 釣り銭機の釣り銭残高
                Console.WriteLine("\n現在の釣り銭機残高");
                for (int i = 0; i < vendingMachine.Stock.Length; i++)
                {
                    Console.WriteLine($"{vendingMachine.Stock[i].Type}円 : {vendingMachine.Stock[i].Count}");
                }
            }

            static VendingMachine setVenderMachine()
            {
                return new VendingMachine(
                    new Money(10000, 10),
                    new Money(5000, 10),
                    new Money(1000, 10),
                    new Money(500, 1),
                    new Money(100, 1),
                    new Money(50, 5),
                    new Money(10, 10),
                    new Money(5, 10),
                    new Money(1, 10));
            }
        }
    }

}

結果

商品価格 230円、預かり金 1,000円と入力した様子

現在の釣り銭機残高
10000円 : 10
5000円 : 10
1000円 : 10
500円 : 1
100円 : 1
50円 : 5
10円 : 10
5円 : 10
1円 : 10

レジの開始

価格230
預かり金1000
商品代金:230円, 預かり金:1000円

払い出す金種と数
10000円 : 0
5000円 : 0
1000円 : 0
500円 : 1
100円 : 1
50円 : 3
10円 : 2
5円 : 0
1円 : 0

現在の釣り銭機残高
10000円 : 10
5000円 : 10
1000円 : 10
500円 : 0
100円 : 0
50円 : 2
10円 : 8
5円 : 10
1円 : 10

テストコード

参考としてxUnitのテストクラスを示します
ソリューションにプロジェクトを追加して(xUnitプロジェクトを選択)、VendingMachineプロジェクトを参照の追加しておきます
長くなっているのは、テスト機能の基本動作についても記述しているためです

namespace VendingMachineTest
{
    using System.Collections;
    using VendingMachine;

    public class UnitTest1
    {
        [Fact(DisplayName = "配列の比較、基本テスト")]
        public void Test1()
        {
            int[] a = new[] { 1, 2 };
            int[] b = new[] { 1, 2 };

            Assert.True(a.SequenceEqual(b));
        }

        [Fact(DisplayName = "配列の比較")]
        public void Test2()
        {
            Money[] money = new[] {
                new Money(10000, 0),
                new Money(5000, 0),
                new Money(1000, 1),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 2),
                new Money(1, 0) };

            Money[] money1 = new[] {
                new Money(10000, 0),
                new Money(5000, 0),
                new Money(1000, 1),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 2),
                new Money(1, 0) };

            Assert.True(money.SequenceEqual(money1));
        }


        [Fact(DisplayName = "釣り銭のテスト")]
        public void Test3()
        {
            VendingMachine venderMachine = setVenderMachine();

            Money[] money = new[] {
                new Money(10000, 0),
                new Money(5000, 0),
                new Money(1000, 1),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 2),
                new Money(1, 0) };


            var test = venderMachine.HandlingChange(340, 2300);
            Assert.True(test.SequenceEqual(money));
        }

        [Theory]
        [ClassData(typeof(CheckChange))]
        public void Test4(VendingMachine v, Money[] money)
        {
            var test = v.HandlingChange(340, 2300);
            Assert.True(test.SequenceEqual(money));
        }

        [Theory]
        [ClassData(typeof(AddCountTest))]
        public void Test5(VendingMachine v, int type, int count, Money[] money)
        {
            var a = v.AddCount(type, count);
            //Assert.True(a);
            Assert.True(money.SequenceEqual(v.Stock));
        }

        static VendingMachine setVenderMachine()
        {
            return new VendingMachine(
                new Money(10000, 10),
                new Money(5000, 10),
                new Money(1000, 10),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 10),
                new Money(1, 10));
        }
    }

    public class AddCountTest : IEnumerable<object[]>
    {
        List<object[]> _testAddData = new List<object[]>();

        public AddCountTest()
        {
            _testAddData.Add(new object[] {
                new VendingMachine(
                new Money(10000, 10),
                new Money(5000, 10),
                new Money(1000, 10),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 10),
                new Money(1, 10)),
            10000,
            100,
            new[]{
                new Money(10000, 110),
                new Money(5000, 10),
                new Money(1000, 10),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 10),
                new Money(1, 10)}  });
        }

        public IEnumerator<object[]> GetEnumerator() => _testAddData.GetEnumerator();
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    }

    // テストデータ作成クラス
    public class CheckChange : IEnumerable<object[]>
    {
        List<object[]> _testData = new List<object[]>();

        public CheckChange()
        {
            _testData.Add(new object[] {
                new VendingMachine(
                new Money(10000, 10),
                new Money(5000, 10),
                new Money(1000, 10),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 10),
                new Money(1, 10)),
            new[]{
                    new Money(10000, 0),
                    new Money(5000, 0),
                    new Money(1000, 1),
                    new Money(500, 1),
                    new Money(100, 1),
                    new Money(50, 5),
                    new Money(10, 10),
                    new Money(5, 2),
                    new Money(1, 0) } });

            _testData.Add(new object[] {
                new VendingMachine(
                new Money(10000, 10),
                new Money(5000, 10),
                new Money(1000, 10),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 10),
                new Money(10, 10),
                new Money(5, 10),
                new Money(1, 10)),
            new[]{
                    new Money(10000, 0),
                    new Money(5000, 0),
                    new Money(1000, 1),
                    new Money(500, 1),
                    new Money(100, 1),
                    new Money(50, 7),
                    new Money(10, 1),
                    new Money(5, 0),
                    new Money(1, 0) } });
        }

        public IEnumerator<object[]> GetEnumerator() => _testData.GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
}

テストサンプル結果

クラス図

金種をListで管理

配列での管理では最初に金種が固定されていますが、Listでの管理では釣り銭機クラスに金種が固定されていません

コード

金種ごとの基本となるクラス

釣り銭機に格納されているお金のストックを作りますが、それぞれの金種の情報をクラスとして情報を持つことにします
このクラスは、配列のパターンと同じです

  • 種別(1000円の場合は、1000)
  • 数量(1000円が3枚であれば3)
namespace VendingMachine
{
    public struct Money
    {
        public Money(int type, int count)
        {
            Type = type;
            Count = count;
        }

        public int Type { get; set; }
        public int Count { get; set; }
    }
}

釣り銭機クラス

釣り銭機は、金種分の格納エリアがあるとします(stock)

コンストラクタで、初期設投入金額を金種ごとの数量として登録します

namespace VendingMachine
{
    public class VendingMachine
    {
        // 格納金
        List<Money>? stock;
        // 現在の格納金の値を一時保管(払い出し不良の場合、この値をstockに戻します)
        List<Money>? tempStock;
        // 釣り銭切れの金種
        List<Money> emptyChanges = new List<Money>();

        public VendingMachine(List<Money> stock)
        {
            this.stock = stock;
        }

        public List<Money> Stock => stock;

        /// <summary>
        /// 釣り銭の処理
        /// </summary>
        /// <param name="price">価格</param>
        /// <param name="deposit">預かり金</param>
        /// <returns>釣り銭</returns>
        public Money[]? HandlingChange(int price, int deposit)
        {
            tempStock = new List<Money>(stock);

            emptyChanges.Clear();

            // 金種ごとの処理
            for (int i = 0; i < stock.Count; i++)
            {
                CalcType(price, ref deposit, i);
            }

            // 釣り銭が足りない
            if (deposit - price != 0)
            {
                stock = tempStock;
                return null;
            }

            var changes = new List<Money>();

            for (int i = 0; i < stock.Count; i++)
            {
                changes.Add(new Money(tempStock[i].Type, tempStock[i].Count - stock[i].Count));
            }

            return changes.ToArray();
        }

        /// <summary>
        /// 釣り銭の種類による払い出し処理
        /// </summary>
        /// <param name="price">価格</param>
        /// <param name="deposit">預かり金</param>
        /// <param name="index">お金の金種</param>
        void CalcType(int price, ref int deposit, int index)
        {
            // 価格が残りの預かり金以上の場合、処理しない
            if (deposit <= price)
            {
                return;
            }

            while (deposit - price >= stock[index].Type)
            {
                // 釣り銭の一部が空の場合
                if (stock[index].Count == 0)
                {
                    emptyChanges.Add(stock[index]);
                    return;
                }

                deposit -= stock[index].Type;

                var ts = stock[index];
                ts.Count--;
                stock[index] = ts;
            }
        }

        /// <summary>
        /// 釣り銭のセット
        /// </summary>
        /// <param name="type">金種</param>
        /// <param name="count">金種ごとの数</param>
        /// <returns>セットできたかどうか</returns>
        public bool SeCount(int type, int count)
        {
            for (int i = 0; i < stock.Count; i++)
            {
                if (stock[i].Type == type)
                {
                    var ts = stock[i];
                    ts.Count = count;
                    stock[i] = ts;

                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// 釣り銭の追加
        /// </summary>
        /// <param name="type">金種</param>
        /// <param name="count">金種ごとの追加数</param>
        /// <returns>追加できたかどうか</returns>
        public bool AddCount(int type, int count)
        {
            for (int i = 0; i < stock.Count; i++)
            {
                if (stock[i].Type == type)
                {
                    var ts = stock[i];
                    ts.Count += count;
                    stock[i] = ts;

                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// 金種ごとの数を取得
        /// </summary>
        /// <param name="type">金種</param>
        /// <returns>金種ごとの数</returns>
        public int GetCount(int type)
        {
            return stock.Find(money => money.Type == type).Count;
            //return Array.Find(stock, money => money.Type == type).Count;
        }
    }
}

釣り銭計算をコントロールするクラス

namespace VendingMachine
{
    class Program
    {
        public static void Main()
        {
            // 釣り銭機に釣り銭をセット
            var vendingMachine = setVenderMachine();
            // 釣り銭機残金表示
            ShowVenderMachineMoneies(vendingMachine);
            // レジ入力
            var inputValue = InputSellData();
            // 計算
            Calc(vendingMachine, inputValue.price, inputValue.deposit);
            // 釣り銭機残金表示
            ShowVenderMachineMoneies(vendingMachine);

            static (int price, int deposit) InputSellData()
            {
                Console.WriteLine("\nレジの開始\n-------------");
                Console.Write("価格");
                var price = int.Parse(Console.ReadLine());
                Console.Write("預かり金");
                var deposit = int.Parse(Console.ReadLine());
                Console.WriteLine($"商品代金:{price}円, 預かり金:{deposit}円\n");

                return (price, deposit);
            }

            static void Calc(VendingMachine vendingMachine, int price, int deposit)
            {
                var ChangeDispensing = vendingMachine.HandlingChange(price, deposit);

                Console.WriteLine("払い出す金種と数");

                if (ChangeDispensing == null)
                {
                    Console.WriteLine("払い出す釣り銭が足らないので処理されません");
                    return;
                }

                for (int i = 0; i < ChangeDispensing.Length; i++)
                {
                    Console.WriteLine($"{ChangeDispensing[i].Type}円 : {ChangeDispensing[i].Count}");
                }
            }

            static void ShowVenderMachineMoneies(VendingMachine vendingMachine)
            {
                // 釣り銭機の釣り銭残高
                Console.WriteLine("\n現在の釣り銭機残高");
                for (int i = 0; i < vendingMachine.Stock.Count; i++)
                {
                    Console.WriteLine($"{vendingMachine.Stock[i].Type}円 : {vendingMachine.Stock[i].Count}");
                }
            }

            static VendingMachine setVenderMachine()
            {
                return new VendingMachine(
                    new List<Money> {
                    new Money(10000, 10),
                    new Money(5000, 10),
                    new Money(1000, 10),
                    new Money(500, 1),
                    new Money(100, 1),
                    new Money(50, 5),
                    new Money(10, 10),
                    new Money(5, 10),
                    new Money(1, 10) });
            }
        }
    }

}

テストコード

長くなっているのは、テスト機能の基本動作についても記述しているためです

namespace VendingMachineTest
{
    using System.Collections;
    using VendingMachine;

    public class UnitTest1
    {
        [Fact(DisplayName = "配列の比較、基本テスト")]
        public void Test1()
        {
            List<int> a = new List<int>() { 1, 2 };
            List<int> b = new List<int>() { 1, 2 };

            Assert.True(a.SequenceEqual(b));
        }

        [Fact(DisplayName = "配列の比較")]
        public void Test2()
        {
            List<Money> money = new List<Money>() {
                new Money(10000, 0),
                new Money(5000, 0),
                new Money(1000, 1),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 2),
                new Money(1, 0) };

            List<Money> money1 = new List<Money>() {
                new Money(10000, 0),
                new Money(5000, 0),
                new Money(1000, 1),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 2),
                new Money(1, 0) };

            Assert.True(money.SequenceEqual(money1));
        }


        [Fact(DisplayName = "釣り銭のテスト")]
        public void Test3()
        {
            VendingMachine venderMachine = setVenderMachine();

            List<Money> money = new List<Money>() {
                new Money(10000, 0),
                new Money(5000, 0),
                new Money(1000, 1),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 2),
                new Money(1, 0) };


            var test = venderMachine.HandlingChange(340, 2300);
            Assert.True(test.SequenceEqual(money));
        }

        [Theory]
        [ClassData(typeof(CheckChange))]
        public void Test4(VendingMachine v, List<Money> money)
        {
            var test = v.HandlingChange(340, 2300);
            Assert.True(test.SequenceEqual(money));
        }

        [Theory]
        [ClassData(typeof(AddCountTest))]
        public void Test5(VendingMachine v, int type, int count, List<Money> money)
        {
            var a = v.AddCount(type, count);
            //Assert.True(a);
            Assert.True(money.SequenceEqual(v.Stock));
        }

        static VendingMachine setVenderMachine()
        {
            return new VendingMachine(
                new List<Money>() {
                new Money(10000, 10),
                new Money(5000, 10),
                new Money(1000, 10),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 10),
                new Money(1, 10) });
        }
    }

    public class AddCountTest : IEnumerable<object[]>
    {
        List<object[]> _testAddData = new List<object[]>();

        public AddCountTest()
        {
            _testAddData.Add(new object[] {
                new VendingMachine(
                new List<Money>(){
                new Money(10000, 10),
                new Money(5000, 10),
                new Money(1000, 10),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 10),
                new Money(1, 10) }),
            10000,
            100,
            new List<Money>(){
                new Money(10000, 110),
                new Money(5000, 10),
                new Money(1000, 10),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 10),
                new Money(1, 10)}  });
        }

        public IEnumerator<object[]> GetEnumerator() => _testAddData.GetEnumerator();
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    }

    // テストデータ作成クラス
    public class CheckChange : IEnumerable<object[]>
    {
        List<object[]> _testData = new List<object[]>();

        public CheckChange()
        {
            _testData.Add(new object[] {
                new VendingMachine(
                new List<Money>(){
                new Money(10000, 10),
                new Money(5000, 10),
                new Money(1000, 10),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 5),
                new Money(10, 10),
                new Money(5, 10),
                new Money(1, 10) }),
                new List<Money>(){
                    new Money(10000, 0),
                    new Money(5000, 0),
                    new Money(1000, 1),
                    new Money(500, 1),
                    new Money(100, 1),
                    new Money(50, 5),
                    new Money(10, 10),
                    new Money(5, 2),
                    new Money(1, 0) } });

            _testData.Add(new object[] {
                new VendingMachine(
                new List<Money>(){
                new Money(10000, 10),
                new Money(5000, 10),
                new Money(1000, 10),
                new Money(500, 1),
                new Money(100, 1),
                new Money(50, 10),
                new Money(10, 10),
                new Money(5, 10),
                new Money(1, 10) }),
                new  List<Money>(){
                    new Money(10000, 0),
                    new Money(5000, 0),
                    new Money(1000, 1),
                    new Money(500, 1),
                    new Money(100, 1),
                    new Money(50, 7),
                    new Money(10, 1),
                    new Money(5, 0),
                    new Money(1, 0) } });
        }

        public IEnumerator<object[]> GetEnumerator() => _testData.GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
}

テストサンプル結果

クラス図

C#

Posted by hidepon