[TDD]Find the Next Bigger Number from Codewars

Codewars 上的 kata: Next bigger number with the same digits

You have to create a function that takes a positive integer number and returns the next bigger number formed by the same digits:
f(12) == 21
f(513) == 531
f(2017) == 2071

If no bigger number can be composed using those digits, return -1:
f(9) == -1
f(111) == -1
f(531) == -1

題目解釋:給一個正整數 input,回傳由相同數字所組成的下一個更大的數字。

本文要透過 TDD 的方式,來解釋我的思路跟程式碼演進過程。

嚴格來說,這個需求用「窮舉法+排序」找下一個更大的值,應該是最快、最直覺的,但這樣比較感受不到 TDD 的過程。所以這個需求,我們將用特定的演算法來求出下一個更大的數字。


第一個紅燈,input = 1, return -1; 因為 1 已經是最大的數字

測試案例用意:先透過第一個測試案例產生對應產品程式碼的骨架。

測試程式碼如下:

    [TestClass]
    public class UnitTest1
    {
        private const int NoLargerNumber = -1;

        [TestMethod]
        public void Test_1_should_be_largestNumber()
        {
            var input = 1;

            var expected = NoLargerNumber;

            int actual = NextLargerNumber.Next(input);

            Assert.AreEqual(expected, actual);
        }
    }

產生出來的產品程式碼如下:

    public static class NextLargerNumber
    {
        public static int Next(int input)
        {
            throw new NotImplementedException();
        }
    }

接著用最簡單的方式通過測試,直接 return -1;

產品程式碼如下:

        public static int Next(int input)
        {
            return -1;            
        }

新增一個測試案例:input = 12, should return 21;

這個測試案例用意,需先處理 input 整數要透過 char 取得各位的數字, swap ,以及回傳時要再把 char 組成十進位的數字。

測試程式如下:

        [TestMethod]
        public void Test_input_is_12_should_return_21()
        {
            var input = 12;
            var expected = 21;

            var actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }

產品程式碼設計概念:

  1. 先將 input 轉成 char array, 再轉成 List<int> 以便後續處理。
  2. 右邊的數字與左邊的數字 swap ,將 swap 完的 List<int> 透過 Select() + Math.Pow() 轉換成十進位數字。
  3. 原本 return -1; 的邏輯,在這先定義為如果 input 產生的 List<int>Count 只有 1 時,則 return -1;(等到別的測試案例因此失敗或重構時,再來調整。)

注意:我刻意略過了一個商業邏輯,應該是當右邊數字比左邊大,才進行 swap 的動作。這一段商業邏輯,我決定用下一個測試案例 input = 21 來補。

產品程式碼如下:

        public static int Next(int input)
        {
            var inputNumbers = input.ToString().ToCharArray().Select(x => (int)Char.GetNumericValue(x)).ToList();

            if (inputNumbers.Count == 1)
            {
                return -1;
            }

            var temp = inputNumbers[1];
            inputNumbers[1] = inputNumbers[0];
            inputNumbers[0] = temp;

            int result = GetNumbericFromValueList(inputNumbers);
            return result;
        }

        private static int GetNumbericFromValueList(List<int> inputNumbers)
        {
            var count = inputNumbers.Count;

            var result = inputNumbers.Select((x, index) => x * (Math.Pow(10, (count - index - 1)))).Sum();
            return Convert.ToInt32(result);
        }

新增下一個測試案例,input=21, should return -1;

這裡有兩種思維,一種是把 21 當作最大值得回傳 -1 來做,合併進去 return -1 的判斷式。另一種則是併入 swap 的判斷式。在這,我決定採後者。因為我期望等等重構時,可以把 return -1 的邏輯也進行重構。

測試案例如下:

        [TestMethod]
        public void Test_input_is_21_should_be_largestNumber()
        {
            var input = 21;
            var expected = NoLargerNumber;
            var actual = NextLargerNumber.Next(input);

            Assert.AreEqual(expected, actual);
        }

產品程式碼設計概念,只有當右邊的數字 > 左邊的數字,才進行 swap。
如果最後處理完的結果,與傳入的 input 相同,代表已為最大值,應回傳 -1 。

產品程式碼如下:

        public static int Next(int input)
        {
            var inputNumbers = input.ToString().ToCharArray().Select(x => (int)Char.GetNumericValue(x)).ToList();

            if (inputNumbers.Count == 1)
            {
                return -1;
            }

            if (inputNumbers[1] > inputNumbers[0])
            {
                var temp = inputNumbers[1];
                inputNumbers[1] = inputNumbers[0];
                inputNumbers[0] = temp;
            }

            int result = GetNumbericFromValueList(inputNumbers);
            return result == input ? -1 : result;
        }

重構

我想先消掉 inputNumbers[1]inputNumbers[0] magic number 的壞味道,所以加上 for loop 以 index 取代 1 與 0。

產品程式碼如下:

        public static int Next(int input)
        {
            var inputNumbers = input.ToString().ToCharArray().Select(x => (int)Char.GetNumericValue(x)).ToList();

            if (inputNumbers.Count == 1)
            {
                return -1;
            }

            for (int index = inputNumbers.Count - 1; index > 0; index--)
            {
                if (inputNumbers[index] > inputNumbers[index - 1])
                {
                    var temp = inputNumbers[index];
                    inputNumbers[index] = inputNumbers[index - 1];
                    inputNumbers[index - 1] = temp;
                }
            }


            int result = GetNumbericFromValueList(inputNumbers);
            return result == input ? -1 : result;
        }

重構

我想消掉第一個 return -1 的判斷式,因為最後的 return 已經有判斷,如果處理完的結果與傳入的 input 相同時,回傳 -1。這一段邏輯是涵蓋了我們 input = 1 的測試案例。

產品程式碼如下:

        public static int Next(int input)
        {
            var inputNumbers = input.ToString().ToCharArray().Select(x => (int)Char.GetNumericValue(x)).ToList();

            for (int index = inputNumbers.Count - 1; index > 0; index--)
            {
                if (inputNumbers[index] > inputNumbers[index - 1])
                {
                    var temp = inputNumbers[index];
                    inputNumbers[index] = inputNumbers[index - 1];
                    inputNumbers[index - 1] = temp;
                }
            }

            int result = GetNumbericFromValueList(inputNumbers);
            return result == input ? -1 : result;
        }

重構

當巡覽 inputNumbers 有發生 swap 動作時,代表與原本的 input 不一樣了,這邊就直接回傳 swap 完的結果值。(因為目前測試案例只有兩位數,所以可以直接回傳)

反之,如果巡覽過程中,左邊全都比右邊大,沒發生任何 swap 動作,代表本身就是最大值,則 return -1;

產品程式碼如下:

        public static int Next(int input)
        {
            var inputNumbers = input.ToString().ToCharArray().Select(x => (int)Char.GetNumericValue(x)).ToList();

            for (int index = inputNumbers.Count - 1; index > 0; index--)
            {
                if (inputNumbers[index] > inputNumbers[index - 1])
                {
                    var temp = inputNumbers[index];
                    inputNumbers[index] = inputNumbers[index - 1];
                    inputNumbers[index - 1] = temp;

                    return GetNumbericFromValueList(inputNumbers);
                }
            }

            return -1;
        }

新增一個預計會通過的測試案例:input = 345, should return 354;

這個測試案例會通過的原因是,第一次的 5 與 4 交換後,剛好是下一個最大值,但 input 已經來到三位數了。

測試案例如下:

        [TestMethod]
        public void Test_input_is_345_should_return_354()
        {
            var input = 345;
            var expected = 354;
            var actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }

新增一個演算法最關鍵的測試案例:input = 576, should return 657;

這個測試案例的用意在於,修正 swap 與處理方式。雖然 input 仍是三位數,但如果照原本的產品程式碼邏輯,最後會回傳是 756,因為第一輪 576 右邊的的 6 比左邊的 7 小,不進行 swap。第二輪右邊的 7 比左邊的 5 大,swap 後的結果是 756。

測試案例如下:

        [TestMethod]
        public void input_is_576_should_return_657()
        {
            var input = 576;
            var expected = 657;
            var actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }

產品程式碼演算法解釋如下:

  1. 從 input 最右邊的位數往左邊一位比較,如果右邊比左邊小,則往下一位 shift 繼續比較。以這例子來說,就是 6 比 7 小,shift 下一位換 7 跟 5 比較。
  2. 當右邊的數字,比左邊大時,我們先把原本的 input 以 L,T,R 來表示。以這例子來說,右邊的 7 比左邊的 5 大,要進行 swap 的處理。這時 T 就是 5,R 就是 {7, 6},L 就是空集合。
  3. 右邊待 swap 的數字,從 R 的集合中,找出大於 T 的最小值。以這例子來說,就是從 {7,6} 找到比 5 大的最小值為 6。
  4. 將找到右邊待 swap 的數字,與 T 交換。
  5. 針對新的 R 做升冪處理,才能確保是最小值的組合。
  6. 最後的結果為 swap 完畢後的 L + T + R。

註:實際的程式碼為了好寫,L 的集合直接先 include T 的值,比較好進行 swap。

產品程式碼如下:

        public static int Next(int input)
        {
            var inputNumbers = input.ToString().ToCharArray().Select(x => (int)Char.GetNumericValue(x)).ToList();

            for (int index = inputNumbers.Count - 1; index > 0; index--)
            {
                var rightFlag = inputNumbers[index];
                var leftFlag = inputNumbers[index - 1];
                if (rightFlag > leftFlag)
                {
                    var t = leftFlag; //暫存 for swap
                    var r = inputNumbers.Skip(index).Take(inputNumbers.Count - index).ToList();
                    var l = inputNumbers.Take(index).ToList(); //包含t

                    for (int i = r.Count - 1; i >= 0; i--)
                    {
                        if (r[i] > t)
                        {
                            l[index - 1] = r[i];
                            r[i] = t;
                            break; //找到第一個可以swap的,就是比t大的最小值
                        }
                    }

                    l.AddRange(r.OrderBy(x => x));

                    return GetNumbericFromValueList(l);
                }
            }

            return -1;
        }

重構

rightFlagleftFlag 變成 inline variable, 消除不必要的變數。(如果你有用 ReSharper, 這個動作用熱鍵就可以執行)

程式碼如下:

        public static int Next(int input)
        {
            var inputNumbers = input.ToString().ToCharArray().Select(x => (int)Char.GetNumericValue(x)).ToList();

            for (int index = inputNumbers.Count - 1; index > 0; index--)
            {
                if (inputNumbers[index] > inputNumbers[index - 1])
                {
                    var t = inputNumbers[index - 1]; //暫存 for swap
                    var r = inputNumbers.Skip(index).Take(inputNumbers.Count - index).ToList();
                    var l = inputNumbers.Take(index).ToList(); //包含t

                    for (int i = r.Count - 1; i >= 0; i--)
                    {
                        if (r[i] > t)
                        {
                            l[index - 1] = r[i];
                            r[i] = t;
                            break; //找到第一個可以swap的,就是比t大的最小值
                        }
                    }

                    l.AddRange(r.OrderBy(x => x));

                    return GetNumbericFromValueList(l);
                }
            }

            return -1;
        }

重構

t 變成 inline variable,消除不必要的變數。移除註解。

產品程式碼如下:

        public static int Next(int input)
        {
            var inputNumbers = input.ToString().ToCharArray().Select(x => (int)Char.GetNumericValue(x)).ToList();

            for (int index = inputNumbers.Count - 1; index > 0; index--)
            {
                if (inputNumbers[index] > inputNumbers[index - 1])
                {                    
                    var r = inputNumbers.Skip(index).Take(inputNumbers.Count - index).ToList();
                    var l = inputNumbers.Take(index).ToList();

                    for (int i = r.Count - 1; i >= 0; i--)
                    {
                        if (r[i] > inputNumbers[index - 1])
                        {
                            l[index - 1] = r[i];
                            r[i] = inputNumbers[index - 1];
                            break;
                        }
                    }

                    l.AddRange(r.OrderBy(x => x));

                    return GetNumbericFromValueList(l);
                }
            }

            return -1;
        }

重構

Skip().Take().ToList() 的部分,改用 List.GetRange() 取代,原因是來源是 List, 最後也要 List,不如直接用 GetRange() 避免不必要的巡覽。

重構完程式碼如下:

        public static int Next(int input)
        {
            var inputNumbers = input.ToString().ToCharArray().Select(x => (int)Char.GetNumericValue(x)).ToList();

            for (int index = inputNumbers.Count - 1; index > 0; index--)
            {
                if (inputNumbers[index] > inputNumbers[index - 1])
                {
                    var r = inputNumbers.GetRange(index, inputNumbers.Count - index);
                    var l = inputNumbers.GetRange(0, index);

                    for (int i = r.Count - 1; i >= 0; i--)
                    {
                        if (r[i] > inputNumbers[index - 1])
                        {
                            l[index - 1] = r[i];
                            r[i] = inputNumbers[index - 1];
                            break;
                        }
                    }

                    l.AddRange(r.OrderBy(x => x));

                    return GetNumbericFromValueList(l);
                }
            }

            return -1;
        }

重構

將 swap 的處理,擷取方法到 Swap()

程式碼如下:

        public static int Next(int input)
        {
            var inputNumbers = input.ToString().ToCharArray().Select(x => (int)Char.GetNumericValue(x)).ToList();

            for (int index = inputNumbers.Count - 1; index > 0; index--)
            {
                if (inputNumbers[index] > inputNumbers[index - 1])
                {
                    var r = inputNumbers.GetRange(index, inputNumbers.Count - index);
                    var l = inputNumbers.GetRange(0, index);

                    Swap(r, inputNumbers, index, l);

                    l.AddRange(r.OrderBy(x => x));

                    return GetNumbericFromValueList(l);
                }
            }

            return -1;
        }

        private static void Swap(List<int> r, List<int> inputNumbers, int index, List<int> l)
        {
            for (int i = r.Count - 1; i >= 0; i--)
            {
                if (r[i] > inputNumbers[index - 1])
                {
                    l[index - 1] = r[i];
                    r[i] = inputNumbers[index - 1];
                    break;
                }
            }
        }

重構

inputNumbers[index - 1] 提取變數到 for loop 外面,且改用 L 集合取代。因為inputNumbers[index-1]L[index-1] 等義。

接著消去未使用到的參數 inputNumbers

程式碼如下:

        public static int Next(int input)
        {
            var inputNumbers = input.ToString().ToCharArray().Select(x => (int)Char.GetNumericValue(x)).ToList();

            for (int index = inputNumbers.Count - 1; index > 0; index--)
            {
                if (inputNumbers[index] > inputNumbers[index - 1])
                {
                    var r = inputNumbers.GetRange(index, inputNumbers.Count - index);
                    var l = inputNumbers.GetRange(0, index);

                    Swap(r, index, l);

                    l.AddRange(r.OrderBy(x => x));

                    return GetNumbericFromValueList(l);
                }
            }

            return -1;
        }

        private static void Swap(List<int> r, int index, List<int> l)
        {
            var t = l[index - 1];
            for (int i = r.Count - 1; i >= 0; i--)
            {
                if (r[i] > t)
                {
                    l[index - 1] = r[i];
                    r[i] = t;
                    break;
                }
            }
        }

最終的產品程式碼

    public static class NextLargerNumber
    {
        public static int Next(int input)
        {
            var inputNumbers = input.ToString().ToCharArray().Select(x => (int)Char.GetNumericValue(x)).ToList();

            for (int index = inputNumbers.Count - 1; index > 0; index--)
            {
                if (inputNumbers[index] > inputNumbers[index - 1])
                {
                    var r = inputNumbers.GetRange(index, inputNumbers.Count - index);
                    var l = inputNumbers.GetRange(0, index);

                    Swap(r, index, l);

                    l.AddRange(r.OrderBy(x => x));

                    return GetNumbericFromValueList(l);
                }
            }

            return -1;
        }

        private static void Swap(List<int> r, int index, List<int> l)
        {
            var t = l[index - 1];
            for (int i = r.Count - 1; i >= 0; i--)
            {
                if (r[i] > t)
                {
                    l[index - 1] = r[i];
                    r[i] = t;
                    break;
                }
            }
        }

        private static int GetNumbericFromValueList(List<int> inputNumbers)
        {
            var count = inputNumbers.Count;

            var result = inputNumbers.Select((x, index) => x * (Math.Pow(10, (count - index - 1)))).Sum();
            return Convert.ToInt32(result);
        }
    }

最終的測試案例集:

    [TestClass]
    public class UnitTest1
    {
        private const int NoLargerNumber = -1;

        [TestMethod]
        public void Test_1_should_be_largestNumber()
        {
            var input = 1;
            var expected = NoLargerNumber;
            int actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void Test_9_should_be_largestNumber()
        {
            var input = 9;
            var expected = NoLargerNumber;
            int actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void Test_input_is_12_should_return_21()
        {
            var input = 12;
            var expected = 21;
            var actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void Test_input_is_21_should_be_largestNumber()
        {
            var input = 21;
            var expected = NoLargerNumber;
            var actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void Test_input_is_111_should_be_largestNumber()
        {
            var input = 111;
            var expected = NoLargerNumber;
            var actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void Test_input_is_531_should_be_largetNumber()
        {
            var input = 531;
            var expected = NoLargerNumber;
            var actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void Test_input_is_345_should_return_354()
        {
            var input = 345;
            var expected = 354;
            var actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void input_is_576_should_return_657()
        {
            var input = 576;
            var expected = 657;
            var actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void Test_11200_should_return_12001()
        {
            var input = 11200;
            var expected = 12001;
            var actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }

        [TestMethod]
        public void Test_15963_should_return_16359()
        {
            var input = 15963;
            var expected = 16359;
            var actual = NextLargerNumber.Next(input);
            Assert.AreEqual(expected, actual);
        }
    }

還有演算法的優化空間,但這個 kata 我想把展示的重點放在 TDD 的 baby step及時進行小範圍重構,有興趣的朋友可以自行再鑽研演算法,例如用 substring 做也行。

結論

看到這麼長一篇文章,紅燈、綠燈、重構、重構、重構,到實現一個完整、乾淨的演算法 ,就知道為什麼這麼少人寫這麼詳細的 TDD 文章了。(笑)

【摘要重點】

  • 從最簡單的測試案例下手。
  • 每次要新增的失敗的測試案例,都應該基於目前的測試案例集(或是指目前 production code 還少哪一個關鍵處理)去延伸設計。一次只做一件最小但最重要的事,對工程師的人性來說相當具有挑戰。
  • 重構一定要及時

【TDD 時必用的工具】

  • 地表最強的 IDE:Visual Studio。
  • Refactor 神兵:ReSharper。
  • 即時進行 TDD 神兵:Alive。(已被微軟買走)
  • Debug 測試案例執行過程神兵:OzCode。

我的 Github commit history

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容