حل یک مسئله نسبتا پیچیده با php

میخواهیم یک برای یک نیازمندی سیستم برنامه ای بهمراه تست به زبان php بنویسیم. این مسئله به این صورت است که در یک دفتر کل شبیه دفاتر حسابداری، بدهکاری یا بستانکاری افراد به همدیگر نوشته میشود. به این شکل هر تراکنش کمی بدون ساختار و بصورت ساده به شکل زیر ثبت در این سیستم میشود:

صدرا به شایان مبلغ ۱۱۰۰۰ تومان بدهکار است.

شایان به صدرا مبلغ ۵۰۰۰تومان بدهکار است.

رضا به مهسا مبلغ ۲۰۰۰تومان بدهکار است.

مهسا از فرامرز مبلغ ۸۰۰۰تومان طلبکار است.

حالا میخواهیم برای این ردیف های اطلاعات، حساب نهایی افراد که برای همه تراکنش هایشان مقدار یک مقدار بدهکار/طلبکار مشخص است را محاسبه کنیم.

ابتدا برای درک بیشتر مسئله، آنرا روی کاغذ حل میکنیم. پرسش اینجاست که چطور باید کل مبالغ برای هر نفر محاسبه کرد. خب واضح است که هر جا بطور مثال نام شایان را دیدیم باید به مبلغ را محاسبه کنیم. حال اگر طلبکار بود به حساب کل او اضافه و اگر بدهکار بود از مبلغ کم میکنیم. در نهایت باید داشته باشیم:

صدرا مبلغ ۶۰۰۰تومان طلبکار است.

شایان مبلغ ۶۰۰۰تومان بدهکار است.

مهسا مبلغ ۱۰۰۰۰تومان طلبکار است.

فرامرز مبلغ ۲۰۰۰تومان بدهکار است.

رضا مبلغ ۸۰۰۰تومان بدهکار است.

حال این اطلاعات را به داده های قابل تفسیر تبدیل میکنیم:

خب برای اینکار ابتدا با استفاده ‍‍‍data proivder یک تست مینویسیم تا چند حالت مختلف را تست کنیم و مطمئن شویم کد برای همه حالات جواب درست میدهد. ابتدا نیاز داریم ‍data proivder را برای حالتهای مختلف بسنجیم.

public function calcProvider()
    {
        $case = [
            ['creditor' => 'A', 'owe' => 'B', 'amount' => 1000],
            ['creditor' => 'A', 'owe' => 'B', 'amount' => 4000],
​
            ['creditor' => 'F', 'owe' => 'E', 'amount' => 2000],
​
            ['creditor' => 'D', 'owe' => 'C', 'amount' => 2000],
            ['creditor' => 'C', 'owe' => 'D', 'amount' => 1000]
        ];
        $result= [
            ['creditor' => 'A', 'owe' => 'B', 'amount' => 5000],
​
            ['creditor' => 'F', 'owe' => 'E', 'amount' => 2000],
​
            ['creditor' => 'D', 'owe' => 'C', 'amount' => 1000]
        ];
        return [
            [$case, $result]
        ];
    }

حال باید تستی بنویسیم تا از متد ‍‍‍‍‍‍‍calcProvider استفاده کند. توجه کنید استفاده از data provider به ما امکان میدهد تا بجای نوشتن چند تست برای حالات مختلف یک تست بنویسیم و برای حالت های مختلف ورودی، خروجی مورد نظر را انتظار داشته باشیم. و حالا تستی که از این ‍‍data provider استفاده کند:

   /**
     * @test
     * @dataProvider calcProvider
     */
    public function it_calculates_members_status_in_ledger($arrays, $expected)
    {
        $ledgerFactory = new LedgerFactory();
        $calcArray = $ledgerFactory->calcStatus($arrays);
        $this->assertEquals($expected, $calcArray);
    }

همانطور که قابل مشاهده است ما برای تست، ورودی و خروجی مشخصی معین نکردیم و حالتهای مختلف آنرا به data provider واگذار کردیم. و انتظار داریم برای همه ورودی های داده شده خروجی های متناظر مورد انتظار را تولید کند.

حال برای پاس کردن تست نیاز به قابل برنامه نویسی کردن آنچه در بالا بعنوان درک مسئله گفتیم داریم. اولین چیزی که واضح است این است که نیاز داریم تا بدانیم مبلغ هر سطر برای یک کاربر باید با کل حساب او جمع شود (طلبکار) یا از آن کم (بدهکار) شود. برای این کار نیاز به یک مرتب سازی آرایه ورودی داریم. به طوری که یک بار آرایه را دور بزنیم و بدهکاری را به مبلغی منفی تغییر دهیم و سپس همه را باهم جمع کنیم. منطق این کار برای هر سطر را ابتدا به کد تبدیل میکنیم(مرحله به مرحله و ابتدا حل کردن کوچک ترین مسئله):

public function sort($item)
    {
        return $item['creditor'] > $item['owe'] ?
            ['creditor' => $item['owe'], 'owe' => $item['creditor'], 'amount' => $item['amount'] * -1] : $item;
    }

با استفاده از کد بالا برای بعضی از مقادیر که شرط درست باشد مقدار را منفی میکنیم ولی باید اینکار را برای کل آرایه ها در نظر گرفت. در واقع باید کل آرایه را یکبار دور بزنیم و مقادیر را بعد از اعمال تابعی که اسم آنرا sort گذاشتیم جایگزین کنیم تا آرایه جدید مرتب شده داشته باشیم. برای این کار بر روی آرایه ها در php متد خوب array_map را داریم. پس:

array_map([$this, 'sort'], $arrays);

حال که مقادیر بدهکار یا طلبکار برای هر ردیف را مشخص کردیم باید حساب هر شخص را که به همدیگر هم بدهکار و طلبکار باشند را به یک سطر جمع زده شده تبدیل کنیم. به ردیف ۴و۵ جدول دقت کنید. باید این قسمت را حل کنیم. برای این کار array_reduce بکار می اید که آرایه مرتب شده را یکبار دور بزنیم و هر جا دوسطر شبیه ۴و۵ دیدیم به یک سطر کاهش دهیم.

addition متدی است یک آرایه خالی را به همراه یک ردیف مشخص دریافت کند. آنرا علامت گذاری کند و توی یک آرایه خالی carry بگذارد.

   public function addition($carry, $item)
    {
        $key = $item['creditor'] . '-' . $item['owe'];
        if (isset($carry[$key])) {
            $carry[$key]['amount'] += $item['amount'];
        } else {
            $carry[$key] = $item;
        }
        return $carry;
    }

سپس این آرایه را با استفاده از array_reduce دور بزنیم و هر جا شرط برقرار بود آنرا با مقدار جدید جابجا کنیم. تا اینجا خروجی چیزی شبیه این خواهد بود:

   public function calcStatus($arrays)
    {
        $sortArray = array_map([$this, 'sort'], $arrays);
        $additionArray = array_reduce($sortArray, [$this, 'addition'], []);    
    }
array:3 [
  "A-B" => array:3 [
    "creditor" => "A"
    "owe" => "B"
    "amount" => 50000
  ]
  "E-F" => array:3 [
    "creditor" => "E"
    "owe" => "F"
    "amount" => -20000
  ]
  "C-D" => array:3 [
    "creditor" => "C"
    "owe" => "D"
    "amount" => -10000
  ]
]

تنها کاری که مانده است این است که از آنجایی که ما نمیخواهیم مقدار منفی به کاربر نشان دهیم این مورد را در آرایه اصلاح کنیم. مثل قبل ابتدا برای هر سطر:

public function reverse($item)
    {
        if ($item['amount'] < 0) {
            return ['creditor' => $item['owe'], 'owe' => $item['creditor'], 'amount' => $item['amount'] * -1];
        } else {
            return $item;
        }
    }

و نهایتا:

   public function calcStatus($arrays)
    {
        $sortArray = array_map([$this, 'sort'], $arrays);
        $additionArray = array_reduce($sortArray, [$this, 'addition'], []);
        $calc = array_values(array_map([$this, 'reverse'], $additionArray));
        return $calc;
        }