<?php
/**
 * iCalcreator, the PHP class package managing iCal (rfc2445/rfc5445) calendar information.
 *
 * This file is a part of iCalcreator.
 *
 * @author    Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
 * @copyright 2007-2022 Kjell-Inge Gustafsson, kigkonsult, All rights reserved
 * @link      https://kigkonsult.se
 * @license   Subject matter of licence is the software iCalcreator.
 *            The above copyright, link, package and version notices,
 *            this licence notice and the invariant [rfc5545] PRODID result use
 *            as implemented and invoked in iCalcreator shall be included in
 *            all copies or substantial portions of the iCalcreator.
 *
 *            iCalcreator is free software: you can redistribute it and/or modify
 *            it under the terms of the GNU Lesser General Public License as
 *            published by the Free Software Foundation, either version 3 of
 *            the License, or (at your option) any later version.
 *
 *            iCalcreator is distributed in the hope that it will be useful,
 *            but WITHOUT ANY WARRANTY; without even the implied warranty of
 *            MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *            GNU Lesser General Public License for more details.
 *
 *            You should have received a copy of the GNU Lesser General Public License
 *            along with iCalcreator. If not, see <https://www.gnu.org/licenses/>.
 */
namespace Kigkonsult\Icalcreator;

use Kigkonsult\Icalcreator\Util\DateTimeFactory;
use Kigkonsult\Icalcreator\Util\RecurFactory;
use Kigkonsult\Icalcreator\Util\RecurFactory2;
use DateTime;
use Exception;

/**
 * class RecurTest, testing selectComponents
 *
 * @since  2.27.20 - 2019-05-20
 */
class RecurMonthTest extends RecurBaseTest
{
    /**
     * recurMonthly1Test provider
     *
     * @return mixed[]
     * @throws Exception
     */
    public function recurMonthly1aProvider() : array
    {
        $dataArr = [];
        $dataSetNo = 0;
        $DATASET = 'DATASET';

        $time = microtime( true );
        $start = DateTimeFactory::factory( '20190105T090000', 'Europe/Stockholm' );
        $wDate = clone $start;
        $expects = [];
        $count = 10;
        $x = 1;
        while( $x < $count ) {
            $wDate = $wDate->setDate(
                (int)$wDate->format( 'Y' ),
                ( (int)$wDate->format( 'm' ) + 1 ),
                (int)$wDate->format( 'd' )
            );
            $expects[] = $wDate->format( 'Ymd' );
            ++$x;
        }
        $execTime = microtime( true ) - $time;
        $dataArr[] = [
            21,
            $start,
            $wDate->modify( RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ => IcalInterface::MONTHLY,
                IcalInterface::COUNT => $count,
                $DATASET => $dataSetNo++
            ],
            $expects,
            $execTime
        ];


        $interval = 1;
        $count = 10;
        for( $ix = 221; $ix <= 229; $ix++ ) {
            $time = microtime( true );
            $start = DateTimeFactory::factory( '20190130T0900', 'Europe/Stockholm' );
            $end = ( clone $start )->modify( RecurFactory::EXTENDYEAR . ' years' );
            $endYmd = $end->format( 'Ymd' );
            $wDate = clone $start;
            $expects = [];
            $x = 1;
            $day = (int)$wDate->format( 'd' );
            $month = (int)$wDate->format( 'm' );
            $year = (int)$wDate->format( 'Y' );
            while( $x < $count ) {
                $month += $interval;
                if( 12 < $month ) {
                    $year += (int)floor( $month / 12 );
                    $month %= 12;
                    if( 0 === $month ) {
                        $month = 12;
                    }
                }
                if( ! checkdate( $month, $day, $year ) ) {
                    continue;
                }
                $Ymd = sprintf( '%04d%02d%02d', $year, $month, $day );
                if( $endYmd < $Ymd ) {
                    break;
                }
                $expects[] = $Ymd;
                ++$x;
            } // end while
            $execTime = microtime( true ) - $time;
            $dataArr[] = [
                $ix . '-' . $interval,
                $start,
                ( clone $start )->modify( RecurFactory::EXTENDYEAR . ' years' ),
                [
                    IcalInterface::FREQ     => IcalInterface::MONTHLY,
                    IcalInterface::INTERVAL => $interval,
                    IcalInterface::COUNT    => $count,
                    $DATASET                => $dataSetNo++
                ],
                $expects,
                $execTime
            ];
            ++$interval;
        }

        $interval = 1;
        $byMonth = [ 1, 5, 12 ];
        $count = 9;
        for( $ix = 231; $ix <= 239; $ix++ ) {
            $time = microtime( true );
            $start = DateTimeFactory::factory( '20190101T0900', 'Europe/Stockholm' );
//            $end     = (clone $start)->modify( RecurFactory::EXTENDYEAR . ' years' );
            $end = ( clone $start )->modify( 5 . ' years' );
            $endYmd = $end->format( 'Ymd' );
            $wDate = clone $start;
            $expects = [];
            $x = 1;
            $day = (int)$wDate->format( 'd' );
            $month = (int)$wDate->format( 'm' );
            $year = (int)$wDate->format( 'Y' );
            while( $x < $count ) {
                $month += $interval;
                if( 12 < $month ) {
                    $year += (int)floor( $month / 12 );
                    $month %= 12;
                    if( 0 === $month ) {
                        $month = 12;
                    }
                }
                if( ! checkdate( $month, $day, $year ) ) {
                    continue;
                }
                $Ymd = sprintf( '%04d%02d%02d', $year, $month, $day );
                if( $endYmd < $Ymd ) {
                    break;
                }
                if( ! in_array( $month, $byMonth ) ) {
                    continue;
                }
                $expects[] = $Ymd;
                ++$x;
            } // end while
            $execTime = microtime( true ) - $time;
            $dataArr[] = [
                $ix . '-' . $interval,
                $start,
                $end,
                [
                    IcalInterface::FREQ     => IcalInterface::MONTHLY,
                    IcalInterface::INTERVAL => $interval,
                    IcalInterface::COUNT    => $count,
                    IcalInterface::BYMONTH  => $byMonth,
                    $DATASET                => $dataSetNo++
                ],
                $expects,
                $execTime,
            ];
            ++$interval;
        }

        $interval   = 1;
        $byMonthDay = [ 1 ];
        $count      = 20;
        $switch     = true;
        for( $ix = 241; $ix <= 249; $ix++ ) {
            $time    = microtime( true );
            $start   = DateTimeFactory::factory( '20190101T0900', 'Europe/Stockholm' );
            $end     = clone $start;
            $end->modify( RecurFactory::EXTENDYEAR . ' years' );
            $endYmd  = $end->format( 'Ymd' );
            $wDate   = clone $start;
            $expects = [];
            $x       = 1;
            $day     = (int)$wDate->format( 'd' );
            $month   = (int)$wDate->format( 'm' );
            $year    = (int)$wDate->format( 'Y' );
            $monthSave      = $month;
            $lastDayInMonth = (int)$wDate->format( 't' );
            $tz = $wDate->getTimezone()->getName();
            while( $x < $count ) {
                if( $month !== $monthSave ) {
                    $month += $interval;
                    if( 12 < $month ) {
                        $year += (int)floor( $month / 12 );
                        $month %= 12;
                        if( 0 === $month ) {
                            $month = 12;
                        }
                    }
                    $monthSave = $month;
                    $day = 1;
                    $date = DateTimeFactory::factory( sprintf( '%04d%02d%02d', $year, $month, $day ), $tz );
                    $lastDayInMonth = (int)$date->format( 't' );
                } // end if
                elseif( $day === $lastDayInMonth ) {
                    $monthSave = -1;
                    continue;
                }
                else {
                    ++$day;
                }
                $match = false;
                foreach( $byMonthDay as $monthDay ) {
                    if( 0 < $monthDay ) {
                        if( $monthDay === $day ) {
                            $match = true;
                            break;
                        }
                    }
                    else if( ( $lastDayInMonth + 1 + $monthDay ) === $day ) {
                        $match = true;
                        break;
                    }
                } // end foreach
                $Ymd = sprintf( '%04d%02d%02d', $year, $month, $day );
                if( $endYmd < $Ymd ) {
                    break;
                }
                if( $match ) {
                    $expects[] = $Ymd;
                    ++$x;
                }
                if( $x >= $count ) {
                    break;
                }
            } // end while
            $execTime = microtime( true ) - $time;
            $dataArr[] = [
                $ix . '-' . $interval,
                $start,
                $end,
                [
                    IcalInterface::FREQ       => IcalInterface::MONTHLY,
                    IcalInterface::INTERVAL   => $interval,
                    IcalInterface::COUNT      => $count,
                    IcalInterface::BYMONTHDAY => $byMonthDay,
                    $DATASET                  => $dataSetNo++
                ],
                $expects,
                $execTime,
            ];
            $interval    += 2;
            $byMonthDay[] = $switch ? ( 0 - $interval ) : $interval;
            $switch       = ! $switch;
        } // end for

        $interval   = 1;
        $byMonthDay = [ 1, 3, 5, 7, -5, -3, -1 ];
        $byMonth    = [ 1, 12 ];
        $count      = 20;
        $switch     = true;
        for( $ix = 251; $ix <= 259; $ix++ ) {
            $time    = microtime( true );
            $start   = DateTimeFactory::factory( '20190101T0900', 'Europe/Stockholm' );
            $end     = ( clone $start )->modify( RecurFactory::EXTENDYEAR . ' years' );
            $endYmd  = $end->format( 'Ymd' );
            $wDate   = clone $start;
            $expects = [];
            $x = 1;
            $day     = (int)$wDate->format( 'd' );
            $month   = (int)$wDate->format( 'm' );
            $year    = (int)$wDate->format( 'Y' );
            $monthSave = $month;
            $lastDayInMonth = (int)$wDate->format( 't' );
            $tz = $wDate->getTimezone()->getName();
            while( $x < $count ) {
                if( $month !== $monthSave ) {
                    $month += $interval;
                    if( 12 < $month ) {
                        $year += (int)floor( $month / 12 );
                        $month %= 12;
                        if( 0 === $month ) {
                            $month = 12;
                        }
                    }
                    if( ! in_array( $month, $byMonth, true ) ) {
                        continue;
                    }
                    $monthSave = $month;
                    if( ! empty( $byMonthDay ) ) {
                        $day = 1;
                        $lastDayInMonth = (int)(
                        DateTimeFactory::factory( sprintf( '%04d%02d%02d', $year, $month, $day ), $tz ) )
                            ->format( 't' );
                    }
                } // end if
                elseif( $day === $lastDayInMonth ) {
                    $monthSave = -1;
                    continue;
                }
                else {
                    ++$day;
                }
                if( ! checkdate( $month, $day, $year ) ) {
                    continue;
                }
                $Ymd = sprintf( '%04d%02d%02d', $year, $month, $day );
                if( $endYmd < $Ymd ) {
                    break;
                }
                $match = false;
                foreach( $byMonthDay as $monthDay ) {
                    if( 0 < $monthDay ) {
                        if( $monthDay === $day ) {
                            $match = true;
                            break;
                        }
                    }
                    else if( ( $lastDayInMonth + 1 + $monthDay ) === $day ) {
                        $match = true;
                        break;
                    }
                } // end foreach
                if( $match ) {
                    $expects[] = $Ymd;
                    ++$x;
                }
            } // end while
            $execTime = microtime( true ) - $time;
            $dataArr[] = [
                $ix,
                $start,
                $end,
                [
                    IcalInterface::FREQ => IcalInterface::MONTHLY,
                    IcalInterface::INTERVAL => $interval,
                    IcalInterface::COUNT => $count,
                    IcalInterface::BYMONTH => $byMonth,
                    IcalInterface::BYMONTHDAY => $byMonthDay,
                    $DATASET => $dataSetNo++
                ],
                $expects,
                $execTime,
            ];
            ++$interval;
            $switch = ! $switch;
        } // end for

        // rfc example 18 (extended) see also #23, above
        $time = microtime( true );
        $start = DateTimeFactory::factory( '20190101T090000', 'Europe/Stockholm' );
        $wDate = clone $start;
        $expects = [];
        $count = 10;
        $x = 1;
        $saveYm = $wDate->format( 'Ym' );
        $wDate = $wDate->modify( '1 day' );
        $mDays = [];
        while( $x < $count ) {
            if( $saveYm !== $wDate->format( 'Ym' ) ) {
                if( ! empty( $mDays ) ) {
                    $expects[] = current( array_slice( $mDays, -3, 1 ) );
                    ++$x;
                    $mDays = [];
                    continue;
                }
                $saveYm = $wDate->format( 'Ym' );
            }
            $mDays[] = $wDate->format( 'Ymd' );
            $wDate = $wDate->modify( '1 day' );
        } // end while
        $execTime = microtime( true ) - $time;
        $dataArr[] = [
            '29-18',
            $start,
            $wDate->modify( RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ => IcalInterface::MONTHLY,
                IcalInterface::COUNT => $count,
                IcalInterface::BYMONTHDAY => -3,
                $DATASET => $dataSetNo++
            ],
            $expects,
            $execTime
        ];

        // rfc example 19 - 20
        $dateString   = '1997-09-02 09:00:00';
        $byMonthDays  = [ 2, 15 ];// rfc 19
        for( $ix = 19; $ix <= 20; $ix++ ) {
            $time     = microtime( true );
            $start    = DateTimeFactory::factory( $dateString, 'America/Los_Angeles' );
            $startYmd = $start->format( 'Ymd' );
            $end      = ( clone $start )->modify( RecurFactory::EXTENDYEAR . ' year' );
            $wDate    = clone $start;
            $expects  = [];
            $count    = 10;
            $x        = 1;
            $year     = (int)$wDate->format( 'Y' );
            $month    = (int)$wDate->format( 'm' );
            while( $x < $count ) {
                $daysInMonth = (int)$wDate->format( 't' );
                foreach( RecurFactory2::getMonthDaysFromByMonthDayList(
                    $daysInMonth, $byMonthDays
                ) as $monthDay ) {
                    $wDate = $wDate->setDate(
                        $year,
                        $month,
                        $monthDay
                    );
                    $Ymd = $wDate->format( 'Ymd' );
                    if( $startYmd >= $Ymd ) {
                        continue;
                    }
                    if( $x >= $count ) {
                        break;
                    }
                    $expects[] = $Ymd;
                    ++$x;
                } // end foreach
                $wDate = $wDate->setDate(
                    $year,
                    $month + 1,
                    1
                );
                $year = (int)$wDate->format( 'Y' );
                $month = (int)$wDate->format( 'm' );
            } // end while
            $execTime = microtime( true ) - $time;
            $dataArr[] = [
                '29-' . $ix,
                $start,
                $end,
                [
                    IcalInterface::FREQ       => IcalInterface::MONTHLY,
                    IcalInterface::COUNT      => $count,
                    IcalInterface::BYMONTHDAY => $byMonthDays,
                    $DATASET                  => $dataSetNo++
                ],
                $expects,
                $execTime
            ];
            // rfc example 20
            $dateString = '1997-09-30 09:00:00';
            $byMonthDays = [ 1, -1 ];
        } // end for

        // rfc example 21
        $time = microtime( true );
        $start = DateTimeFactory::factory( '20190101T090000', 'Europe/Stockholm' );
        $wDate = clone $start;
        $expects = [];
        $count = 10;
        $interval = 18;
        $byMonthDay = range( 10, 15 );
        $x = 1;
        $wDate = $wDate->modify( '1 day' );
        while( $x < $count ) {
            if( 10 > (int)$wDate->format( 'd' ) ) {
                $wDate = $wDate->modify( '1 day' );
            }
            elseif( in_array( (int)$wDate->format( 'd' ), $byMonthDay, true ) ) {
                $expects[] = $wDate->format( 'Ymd' );
                ++$x;
                $wDate = $wDate->modify( '1 day' );
            }
            else {
                $wDate = $wDate->setDate( // interval=18
                    (int)$wDate->format( 'Y' ),
                    ( (int)$wDate->format( 'm' ) + 18 ),
                    10
                );
            }
        } // end while
        $execTime = microtime( true ) - $time;
        $dataArr[] = [
            '29-21-18',
            $start,
            ( clone $start )->modify( RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ => IcalInterface::MONTHLY,
                IcalInterface::COUNT => $count,
                IcalInterface::INTERVAL => $interval,
                IcalInterface::BYMONTHDAY => $byMonthDay,
                $DATASET => $dataSetNo++
            ],
            $expects,
            $execTime
        ];

        // rfc example 21 BUT only day WE
        $time     = microtime( true );
        $start    = DateTimeFactory::factory( '20200801T090000', 'Europe/Stockholm' );
        $wDate    = clone $start;
        $expects  = [];
        $count    = 10;
        $interval = 2;
        $byMonthDay = range( 10, 15 );
        $x = 1;
        $wDate = $wDate->modify( '1 day' );
        while( $x < $count ) {
            $day = (int)$wDate->format( 'j' );
            if( 10 > $day ) {
                $wDate = $wDate->modify( '1 day' );
                continue;
            }
            if( 18 < $day ) {
                $wDate = $wDate->setDate( // interval=2
                    (int)$wDate->format( 'Y' ),
                    ( (int)$wDate->format( 'm' ) + $interval ),
                    10
                );
                continue;
            }
            if( in_array( $day, $byMonthDay, true ) &&
                ( 3 === (int)$wDate->format( 'w' ) ) ) {
                $expects[] = $wDate->format( 'Ymd' );
                ++$x;
            }
            $wDate = $wDate->modify( '1 day' );
        } // end while
        $execTime = microtime( true ) - $time;
        $dataArr[] = [
            '29-21-18B',
            $start,
            ( clone $start )->modify( RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ => IcalInterface::MONTHLY,
                IcalInterface::COUNT => $count,
                IcalInterface::INTERVAL => $interval,
                IcalInterface::BYMONTHDAY => $byMonthDay,
                IcalInterface::BYDAY => [ 'DAY' => 'WE' ],
                $DATASET => $dataSetNo++
            ],
            $expects,
            $execTime
        ];
        return $dataArr;
    }

    /**
     * Testing recurMonthly1 without BYSETPOS
     *
     * @test
     * @dataProvider recurMonthly1aProvider
     * @param int|string  $case
     * @param DateTime    $start
     * @param DateTime    $end
     * @param mixed[]     $recur
     * @param mixed[]     $expects
     * @param float       $prepTime
     * @throws Exception
     */
    public function recurMonthly1aTest(
        int | string $case,
        DateTime     $start,
        DateTime     $end,
        array        $recur,
        array        $expects,
        float        $prepTime
    ) : void
    {
        $this->recurMonthly1XTest(
            $case,
            $start,
            $end,
            $recur,
            $expects,
            $prepTime
        );
    }

    /**
     * recurMonthly1bTest provider
     *
     * @return mixed[]
     * @throws Exception
     */
    public function recurMonthly1bProvider() : array
    {
        $dataArr   = [];
        $dataSetNo = 0;
        $DATASET   = 'DATASET';

        $count    = 10;
        $interval = 2;

        // rfc example 21 BUT only FIRST day WE in extended day period 8-22
        $start      = DateTimeFactory::factory( '20200801T090000', 'Europe/Stockholm');
        $byMonthDay = range( 8,22 );
        $expects    = [
            20200812, 20201014, 20201209, 20210210, 20210414, 20210609, 20210811, 20211013, 20211208
        ];
        $dataArr[] = [
            '29-21-18C',
            $start,
            (clone $start)->modify(  RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ       => IcalInterface::MONTHLY,
                IcalInterface::COUNT      => $count,
                IcalInterface::INTERVAL   => $interval,
                IcalInterface::BYMONTHDAY => $byMonthDay,
                IcalInterface::BYDAY      => [ 'DAY' => 'WE' ],
                IcalInterface::BYSETPOS   => 1,
                $DATASET                  => $dataSetNo++
            ],
            $expects,
            0.0
        ];

        // rfc example 21 BUT only LAST day WE in extended day period 8-22
        $start      = DateTimeFactory::factory( '20200801T090000', 'Europe/Stockholm');
        $byMonthDay = range( 8,22 );
        $expects    = [
            20200819, 20201021, 20201216, 20210217, 20210421, 20210616, 20210818, 20211020, 20211222
        ];
        $dataArr[] = [
            '29-21-18D',
            $start,
            (clone $start)->modify(  RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ       => IcalInterface::MONTHLY,
                IcalInterface::COUNT      => $count,
                IcalInterface::INTERVAL   => $interval,
                IcalInterface::BYMONTHDAY => $byMonthDay,
                IcalInterface::BYDAY      => [ 'DAY' => 'WE' ],
                IcalInterface::BYSETPOS   => -1,
                $DATASET              => $dataSetNo++
            ],
            $expects,
            0.0
        ];

        // rfc example 21 BUT only second day WE in extended day period 8-22
        $start      = DateTimeFactory::factory( '20200801T090000', 'Europe/Stockholm');
        $byMonthDay = range( 8,22 );
        $expects    = [
            20200819, 20201021, 20201216, 20210217, 20210421, 20210616, 20210818, 20211020, 20211215
        ];
        $dataArr[] = [
            '29-21-18E',
            $start,
            (clone $start)->modify(  RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ       => IcalInterface::MONTHLY,
                IcalInterface::COUNT      => $count,
                IcalInterface::INTERVAL   => $interval,
                IcalInterface::BYMONTHDAY => $byMonthDay,
                IcalInterface::BYDAY      => [ 'DAY' => 'WE' ],
                IcalInterface::BYSETPOS   => 2,
                $DATASET              => $dataSetNo++
            ],
            $expects,
            0.0
        ];

        // rfc example 21 BUT only LAST second day WE in extended day period 8-22
        $start      = DateTimeFactory::factory( '20200801T090000', 'Europe/Stockholm');
        $byMonthDay = range( 8,22 );
        $expects    = [
            20200812, 20201014, 20201209, 20210210, 20210414, 20210609, 20210811, 20211013, 20211215
        ];
        $dataArr[] = [
            '29-21-18F',
            $start,
            (clone $start)->modify(  RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ       => IcalInterface::MONTHLY,
                IcalInterface::COUNT      => $count,
                IcalInterface::INTERVAL   => $interval,
                IcalInterface::BYMONTHDAY => $byMonthDay,
                IcalInterface::BYDAY      => [ 'DAY' => 'WE' ],
                IcalInterface::BYSETPOS   => -2,
                $DATASET              => $dataSetNo++
            ],
            $expects,
            0.0
        ];

        // Latest second and third workday in month before day 26 for onr year
        $start      = DateTimeFactory::factory( '20000701T090000', 'Europe/Stockholm');
        $byMonthDay = range( 18,25 );
        $expects    = [
            20000719,20000725,20000821,20000825,20000919,20000925,20001019,20001025,20001120,20001124,20001219,20001225,
            20010119,20010125,20010219,20010223,20010319,20010323,20010419,20010425,20010521,20010525,20010619
        ];
        $dataArr[] = [
            '31',
            $start,
            (clone $start)->modify(  RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ       => IcalInterface::MONTHLY,
                IcalInterface::COUNT      => 24,
                IcalInterface::BYMONTHDAY => $byMonthDay,
                IcalInterface::BYDAY      => [
                    [ 'DAY' => 'MO' ],
                    [ 'DAY' => 'TU' ],
                    [ 'DAY' => 'WE' ],
                    [ 'DAY' => 'TH' ],
                    [ 'DAY' => 'FR' ],
                ],
                IcalInterface::BYSETPOS   => [ -1, -5 ],
                $DATASET                  => $dataSetNo++
            ],
            $expects,
            0.0
        ];

        return $dataArr;
    }


    /**
     * Testing recurMonthly1 with BYSETPOS
     *
     * @test
     * @dataProvider recurMonthly1bProvider
     * @param int|string  $case
     * @param DateTime    $start
     * @param DateTime    $end
     * @param mixed[]     $recur
     * @param mixed[]     $expects
     * @param float       $prepTime
     * @throws Exception
     */
    public function recurMonthly1bTest(
        int | string $case,
        DateTime     $start,
        DateTime     $end,
        array        $recur,
        array        $expects,
        float        $prepTime
    ) : void
    {
        $this->recurMonthly1XTest(
            $case,
            $start,
            $end,
            $recur,
            $expects,
            $prepTime
        );
    }
    /**
     * Testing recurMonthly1 with/without BYSETPOS
     *
     * @dataProvider recurMonthly1aProvider
     * @param int|string $case
     * @param DateTime    $start
     * @param DateTime    $end
     * @param mixed[]     $recur
     * @param mixed[]     $expects
     * @param float $prepTime
     * @throws Exception
     */
    public function recurMonthly1XTest(
        int | string $case,
        DateTime     $start,
        DateTime     $end,
        array        $recur,
        array        $expects,
        float        $prepTime
    ) : void
    {
        $saveStartDate = clone $start;
        $strCase       = str_pad( $case, 12 );

        if( in_array( $case, [ '29-21-18C', '29-21-18D', '29-21-18E', '29-21-18F', '31' ], true ) ) {
            $result = array_flip( $expects );
            if( defined( 'DISPRECUR' ) && ( '1' === DISPRECUR )) {
                echo $strCase . 'expects    time:' . number_format( $prepTime, 6 ) .
                    ' : ' . implode( ' - ', $expects ) . ' count: ' . count( $expects ) . PHP_EOL; // test ###
            }
        }
        else {
            $result = $this->recur2dateTest(
                $case,
                $start,
                $end,
                $recur,
                $expects,
                $prepTime
            );
        }

        if( ! isset( $recur[IcalInterface::INTERVAL] )) {
            $recur[IcalInterface::INTERVAL] = 1;
        }
        $recurDisp = str_replace( [PHP_EOL, ' ' ], '', var_export( $recur, true ));
        if( ! RecurFactory2::isRecurMonthly1( $recur )) {
            if( defined( 'DISPRECUR' ) && ( '1' === DISPRECUR )) {
                echo $strCase . ' NOT isRecurMonthly1 ' . $recurDisp . PHP_EOL;
            }
            $this->fail();
        } // end if
        $time     = microtime( true );
        $resultX  = RecurFactory2::recurMonthly1( $recur, $start, clone $start, $end );
        $execTime = microtime( true ) - $time;
        if( defined( 'DISPRECUR' ) && ( '1' === DISPRECUR )) {
            echo $strCase . 'mnth smpl1 time:' . number_format( $execTime, 6 ) . ' : ' .
                implode( ' - ', array_keys( $resultX ) ) . ' count: ' . count( $resultX ) . PHP_EOL; // test ###
            echo $recurDisp . ' start ' . $start->format( 'Ymd' ) . ' end ' . $end->format( 'Ymd' ) . PHP_EOL; // test ###
        }
        $this->assertEquals(
            array_keys( $result ),
            array_keys( $resultX ),
            sprintf( self::$ERRFMT, __FUNCTION__, $case . '-21',
                $saveStartDate->format( 'Ymd' ),
                $end->format( 'Ymd' ),
                $recurDisp
            )
        );
    }

    /**
     * getRecurByDaysInMonthTest provider
     *
     * @return mixed[]
     */
    public function getRecurByDaysInMonthProvider() : array
    {
        $dataArr = [];

        $dataArr[] = [
            11,
            [ IcalInterface::DAY => IcalInterface::MO ],
            2020,
            8,
            [ 3, 10, 17, 24, 31 ] // exp
        ];

        $dataArr[] = [
            21,
            [ 3, IcalInterface::DAY => IcalInterface::MO ],
            2020,
            8,
            [ 17 ] // exp
        ];

        $dataArr[] = [
            22,
            [ -2, IcalInterface::DAY => IcalInterface::MO ],
            2020,
            8,
            [ 24 ] // exp
        ];

        $dataArr[] = [
            23,
            [ -2, IcalInterface::DAY => IcalInterface::SA ],
            2020,
            2,
            [ 22 ] // exp
        ];

        $dataArr[] = [
            31,
            [
                [ IcalInterface::DAY => IcalInterface::MO ],
                [ IcalInterface::DAY => IcalInterface::FR ],
            ],
            2020,
            8,
            [ 3, 7, 10, 14, 17, 21, 24, 28, 31 ] // exp
        ];

        $dataArr[] = [
            41,
            [
                [ 3, IcalInterface::DAY => IcalInterface::MO ],
                [ -3, IcalInterface::DAY => IcalInterface::FR ],
            ],
            2020,
            8,
            [ 14, 17 ] // exp
        ];

        $dataArr[] = [
            42,
            [
                [ 1, IcalInterface::DAY => IcalInterface::MO ],
                [ 2, IcalInterface::DAY => IcalInterface::TU ],
                [ 3, IcalInterface::DAY => IcalInterface::WE ],
                [ 4, IcalInterface::DAY => IcalInterface::TH ],
                [ 5, IcalInterface::DAY => IcalInterface::FR ],
            ],
            2020,
            8,
            [ 3, 11, 19, 27 ] // exp
        ];

        $dataArr[] = [
            43,
            [
                [ -1, IcalInterface::DAY => IcalInterface::SU ],
                [ -2, IcalInterface::DAY => IcalInterface::SA ],
                [ -3, IcalInterface::DAY => IcalInterface::FR ],
                [ -4, IcalInterface::DAY => IcalInterface::TH ],
                [ -5, IcalInterface::DAY => IcalInterface::WE ],
            ],
            2020,
            8,
            [ 6, 14, 22, 30 ] // exp
        ];

        $dataArr[] = [
            44,
            [
                [ 1, IcalInterface::DAY => IcalInterface::MO ],
                [ 2, IcalInterface::DAY => IcalInterface::TU ],
                [ 3, IcalInterface::DAY => IcalInterface::WE ],
                [ 4, IcalInterface::DAY => IcalInterface::TH ],
                [ 5, IcalInterface::DAY => IcalInterface::FR ],
                [ -1, IcalInterface::DAY => IcalInterface::SU ],
                [ -2, IcalInterface::DAY => IcalInterface::SA ],
                [ -3, IcalInterface::DAY => IcalInterface::FR ],
                [ -4, IcalInterface::DAY => IcalInterface::TH ],
                [ -5, IcalInterface::DAY => IcalInterface::WE ],
            ],
            2020,
            8,
            [ 3, 6, 11, 14, 19, 22, 27, 30 ] // exp
        ];

        return $dataArr;
    }

    /**
     * @test
     * @dataProvider getRecurByDaysInMonthProvider
     * @param int|string $case
     * @param mixed[] $recurByDay
     * @param int $year
     * @param int $month
     * @param mixed[] $exp
     * @throws Exception
     */
    public function getRecurByDaysInMonthTest( int | string $case, array $recurByDay, int $year, int $month, array $exp ) : void
    {
        $list = RecurFactory2::getRecurByDaysInMonth( $recurByDay, $year, $month );

        $this->assertEquals( $exp, $list,
            $case . ' exp: ' .implode( ', ', $exp ) . ' - got: ' .implode( ', ', $list )
        );

        if( defined( 'DISPRECUR' ) && ( '1' === DISPRECUR )) {
            echo $case . ' : ' . $year . ' - ' . $month . ' recur : ' . str_replace( [ PHP_EOL, ' ' ], '', var_export( $recurByDay, true ) ) . PHP_EOL; // test ###
            echo $case . ' result : ' . implode( ', ', $list ) . PHP_EOL; // test ###
        }
    }

    /**
     * recurMonthly1Test provider
     *
     * @return mixed[]
     * @throws Exception
     */
    public function recurMonthly2Provider() : array
    {
        $dataArr   = [];
        $dataSetNo = 0;
        $DATASET   = 'DATASET';

        $start   = DateTimeFactory::factory( '20190101T0900', 'Europe/Stockholm');
        $wDate   = clone $start;
        $expects = [
            20190105, 20190106, 20190112, 20190113, 20190119, 20190120, 20190126, 20190127, 20190202, 20190203,
            20190209, 20190210, 20190216, 20190217, 20190223, 20190224, 20190302, 20190303, 20190309
        ];
        $count   = 20;
        $dataArr[] = [
            '26',
            $start,
            $wDate->modify(  RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ      => IcalInterface::MONTHLY,
                IcalInterface::COUNT     => $count,
                IcalInterface::BYDAY     => [
                    [ IcalInterface::DAY => IcalInterface::SA ],
                    [ IcalInterface::DAY => IcalInterface::SU ]
                ],
                $DATASET             => $dataSetNo++
            ],
            $expects
        ];

        // rfc example 14
        $start   = DateTimeFactory::factory( '20190101T090000', 'Europe/Stockholm');
        $wDate   = clone $start;
        $expects = [ 20190104, 20190201, 20190301, 20190405, 20190503, 20190607, 20190705, 20190802, 20190906, 20191004 ];
        $count   = 10;
        $dataArr[] = [
            '29-14',
            $start,
            $wDate->modify(  RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ     => IcalInterface::MONTHLY,
                IcalInterface::COUNT    => $count,
                IcalInterface::BYDAY    => [ 1, IcalInterface::DAY => IcalInterface::FR ],
                $DATASET            => $dataSetNo++
            ],
            $expects
        ];

        // rfc example 16
        $start   = DateTimeFactory::factory( '20190101T090000', 'Europe/Stockholm');
        $wDate   = clone $start;
        $expects = [ 20190104, 20190125, 20190301, 20190329, 20190503, 20190531, 20190705, 20190726, 20190906, 20190927 ];
        $count   = 10;
        $dataArr[] = [
            '29-16-2',
            $start,
            $wDate->modify(  RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ       => IcalInterface::MONTHLY,
                IcalInterface::INTERVAL   => 2,
                IcalInterface::COUNT      => $count,
                IcalInterface::BYDAY      => [
                    [  1, IcalInterface::DAY => IcalInterface::FR ],
                    [ -1, IcalInterface::DAY => IcalInterface::FR ],
                ],
                $DATASET              => $dataSetNo++
            ],
            $expects
        ];

        // rfc example 17
        $start   = DateTimeFactory::factory( '20190101T090000', 'Europe/Stockholm');
        $wDate   = clone $start;
        $expects = [ 20190121, 20190218, 20190318, 20190422, 20190520, 20190617 ];
        $count   = 6;
        $dataArr[] = [
            '29-17',
            $start,
            $wDate->modify(  RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ     => IcalInterface::MONTHLY,
                IcalInterface::COUNT    => $count,
                IcalInterface::BYDAY    => [ -2, IcalInterface::DAY => IcalInterface::MO ],
                $DATASET            => $dataSetNo++
            ],
            $expects
        ];

        // rfc line 2375
        $start   = DateTimeFactory::factory( '20190101T090000', 'Europe/Stockholm');
        $wDate   = clone $start;
        $expects = [ 20190131, 20190228, 20190329, 20190430, 20190531 ]; // all but first
        $count   = 6;
        $dataArr[] = [
            '2375',
            $start,
            $wDate->modify(  RecurFactory::EXTENDYEAR . ' year' ),
            [
                IcalInterface::FREQ     => IcalInterface::MONTHLY,
                IcalInterface::COUNT    => $count,
                IcalInterface::BYDAY    => [
                    [ IcalInterface::DAY => IcalInterface::MO ],
                    [ IcalInterface::DAY => IcalInterface::TU ],
                    [ IcalInterface::DAY => IcalInterface::WE ],
                    [ IcalInterface::DAY => IcalInterface::TH ],
                    [ IcalInterface::DAY => IcalInterface::FR ],
                ],
                IcalInterface::BYSETPOS => -1,
                $DATASET            => $dataSetNo++
            ],
            $expects
        ];

        // rfc line 2375 but third and last third workday in month
        $start   = DateTimeFactory::factory( '20190101T090000', 'Europe/Stockholm');
        $wDate   = clone $start;
        $expects = [ 20190103, 20190129, 20190205, 20190226, 20190305, 20190327, 20190403, 20190426, 20190503 ]; // all but first
        $count   = 10;
        $dataArr[] = [
            '2375B',
            $start,
            $wDate->modify(  RecurFactory::EXTENDYEAR . ' year' ), // end
            [
                IcalInterface::FREQ     => IcalInterface::MONTHLY,
                IcalInterface::COUNT    => $count,
                IcalInterface::BYDAY    => [
                    [ IcalInterface::DAY => IcalInterface::MO ],
                    [ IcalInterface::DAY => IcalInterface::TU ],
                    [ IcalInterface::DAY => IcalInterface::WE ],
                    [ IcalInterface::DAY => IcalInterface::TH ],
                    [ IcalInterface::DAY => IcalInterface::FR ],
                ],
                IcalInterface::BYSETPOS => [ 3, -3 ],
                $DATASET            => $dataSetNo++
            ],
            $expects
        ];

        // rfc line 7266 but timezone sthlm and count 6
        $start   = DateTimeFactory::factory( '19970929T090000', 'Europe/Stockholm');
        $wDate   = clone $start;
        $expects = [ 19971030, 19971127, 19971230, 19980129, 19980226 ];
        $dataArr[] = [
            '7266',
            $start,
            $wDate->modify(  RecurFactory::EXTENDYEAR . ' year' ), // end
            [
                IcalInterface::FREQ     => IcalInterface::MONTHLY,
                IcalInterface::COUNT    => 6,
                IcalInterface::BYDAY    => [
                    [ IcalInterface::DAY => IcalInterface::MO ],
                    [ IcalInterface::DAY => IcalInterface::TU ],
                    [ IcalInterface::DAY => IcalInterface::WE ],
                    [ IcalInterface::DAY => IcalInterface::TH ],
                    [ IcalInterface::DAY => IcalInterface::FR ],
                ],
                IcalInterface::BYSETPOS => -2,
                $DATASET            => $dataSetNo++
            ],
            $expects
        ];

        // rfc line 7258 but timezone sthlm and count 6
        $start   = DateTimeFactory::factory( '19970904T090000', 'Europe/Stockholm');
        $wDate   = clone $start;
        $expects = [ 19971007,19971106,19971204,19980107,19980205 ];
        $dataArr[] = [
            '7258',
            $start,
            $wDate->modify(  RecurFactory::EXTENDYEAR . ' year' ), // end
            [
                IcalInterface::FREQ      => IcalInterface::MONTHLY,
                IcalInterface::COUNT     => 6,
                IcalInterface::BYDAY     => [
                    [ IcalInterface::DAY => IcalInterface::TU ],
                    [ IcalInterface::DAY => IcalInterface::WE ],
                    [ IcalInterface::DAY => IcalInterface::TH ],
                    ],
                IcalInterface::BYSETPOS  => 3,
                $DATASET             => $dataSetNo++
            ],
            $expects
        ];

        // neotsn Thanksgiving event - 4th Thursday of every November - Yearly - same in recurYearly2Test by YEARLY
        $start   = DateTimeFactory::factory( '20201126T113000', 'America/Chicago');
        $wDate   = clone $start;
        $dataArr[] = [
            'neotsn',
            $start,
            $wDate->modify(  10 . ' year' ), // end
            [
                IcalInterface::FREQ      => IcalInterface::MONTHLY,
                IcalInterface::INTERVAL => 12,
                IcalInterface::BYDAY     => [
                    [ IcalInterface::DAY => IcalInterface::TH ],
                ],
                IcalInterface::BYSETPOS  => 4,
                $DATASET             => $dataSetNo++
            ],
            [ 20211125,20221124,20231123,20241128,20251127,20261126,20271125,20281123,20291122 ]
        ];

        return $dataArr;
    }

    /**
     * Testing recurMonthly2 i.e recurMonthlyYearly3
     *
     * @test
     * @dataProvider recurMonthly2Provider
     * @param string $case
     * @param DateTime $start
     * @param DateTime|array $end
     * @param array    $recur
     * @param array    $expects
     * @throws Exception
     */
    public function recurMonthly2Test(
        string           $case,
        DateTime         $start,
        DateTime | array $end,
        array            $recur,
        array            $expects
    ) : void
    {
        $saveStartDate = clone $start;

        $result = $expects;

        if( ! isset( $recur[IcalInterface::INTERVAL] )) {
            $recur[IcalInterface::INTERVAL] = 1;
        }
        $strCase   = str_pad( $case, 12 );
        $recurDisp = str_replace( [PHP_EOL, ' ' ], '', var_export( $recur, true ));
        if( ! RecurFactory2::isRecurMonthly2( $recur )) {
            if( defined( 'DISPRECUR' ) && ( '1' === DISPRECUR )) {
                echo $strCase . ' NOT isRecurMonthly2 ' . $recurDisp . PHP_EOL;
            }
            $this->fail();
        } // end if
        $time     = microtime( true );
//      $resultX  = RecurFactory2::recurMonthly2( $recur, $start, clone $start, $end );
        $resultX  = RecurFactory2::recurMonthlyYearly3( $recur, $start, clone $start, $end );
        $execTime = microtime( true ) - $time;
        if( defined( 'DISPRECUR' ) && ( '1' === DISPRECUR )) {
            echo $strCase . 'mnth smpl2 time:' . number_format( $execTime, 6 ) . ' : ' .
                implode( ' - ', array_keys( $resultX ) ) . PHP_EOL; // test ###
            echo $recurDisp . PHP_EOL; // test ###
        }
        $this->assertEquals(
            $result, // array_keys( $result ),
            array_keys( $resultX ),
            sprintf( self::$ERRFMT, __FUNCTION__, $case . '-21',
                $saveStartDate->format( 'Ymd' ),
                $end->format( 'Ymd' ),
                PHP_EOL . $recurDisp .
                PHP_EOL . 'exp : ' . implode( ',', $expects ) .
                PHP_EOL . 'got : ' . implode( ',', array_keys( $resultX ))
            )
        );
    }
}