查找某个日期是否在另一个日期之间的两个周末的范围内

时间:2019-02-23 14:57:33

标签: algorithm date methods

我正在制作一个为用户提供折扣的应用程序,其中一个折扣是在他的生日那天给的。由于很难仅在生日那天使用折扣,因此我们决定在整个星期(包括两个周末)给予折扣。

一些规则

  • 给定生日,折扣将从生日的前一个星期五开始;
  • 折扣将在开始后10天(星期日)结束;
  • 如果生日是星期五,那么仍然要获得这个星期五之前的最后一个星期五;

好吧,所以我们需要一个方法来接收birthdayDatetodayDate并返回true或false来告知日期是否在生日范围内。

问题

一切都很好,直到您开始查看临近年份的日期为止。
如果您的生日是2018年12月31日,则可以在2019年1月6日使用折扣,如果您的生日是2019年1月1日,则可以在2018年12月28日使用折扣。
因此,仅查看当前年份的生日是不够的,并且由于每年的星期几在每年更改,因此您今年生日的最后一个星期五将不是下一年的同一天。

有一种优雅的方法可以轻松找到此范围并创建返回true或false的方法?

真正的问题是找到哪个生日与生日相关,因为您可以在上一个生日之后到下一个生日之前获得折扣。

原始代码是用PHP编写的,但是由于它是一个算法问题,我可以接受任何语言的答案

测试用例

| Today             | Birth             | Output |
|-------------------|-------------------|:------:|
| February 15, 2019 | February 22, 2000 | true   |
| February 24, 2019 | February 22, 2000 | true   |
| February 25, 2019 | February 22, 2000 | false  |
| December 28, 2018 | January 03, 2000  | true   |
| December 27, 2018 | January 03, 2000  | false  |
| December 27, 2019 | January 03, 2000  | true   |
| January 01, 2019  | January 03, 2000  | true   |
| January 01, 2019  | December 31, 2000 | true   |
| January 01, 2019  | December 28, 2000 | false  |

3 个答案:

答案 0 :(得分:4)

这是一些使用我的Noda Time库的C#代码,它们通过了您指定的所有测试。

基本思想很简单:

  • 从今天回到三个月
  • 找到该日期之后的生日
  • 构建该生日前后的折扣期
  • 检查今天的日期是否在那个时期

三个月的选择有些随意;它只是用来解决您刚刚生日的情况。 (有可能超过10天且少于355天都是可以的。我只觉得容易推断3个月。)

using System;
using NodaTime;
using NodaTime.Text;

class Test
{
    static void Main()
    {
        RunTest("2019-02-15", "2000-02-22", true);
        RunTest("2019-02-24", "2000-02-22", true);
        RunTest("2019-02-25", "2000-02-22", false);
        RunTest("2018-12-28", "2000-01-03", true);
        RunTest("2019-01-01", "2000-01-03", true);
        RunTest("2018-12-27", "2000-01-03", false);
        RunTest("2019-12-27", "2000-01-03", true);
    }

    static void RunTest(string todayText, string birthdayText, bool expectedResult)
    {
        var pattern = LocalDatePattern.Iso;
        RunTest(pattern.Parse(todayText).Value,
                pattern.Parse(birthdayText).Value,
                expectedResult);               
    }

    static void RunTest(LocalDate today, LocalDate birthday, bool expectedResult)
    {
        // Work out "the birthday that comes after 3 months ago".
        // That can be:
        // - A recent birthday before the closest birthday discount period,
        //   in which case the *next* birthday will be after the current closest
        //   discount period
        // - A birthday *in* the current closest birthday discount period
        // - A birthday *after* the current closest birthday discount period,
        //   in which case the *previous* birthday was before the current
        //   closest discount period
        LocalDate threeMonthsAgo = today.PlusMonths(-3);
        int ageThreeMonthsAgo = Period.Between(birthday, threeMonthsAgo).Years;

        // Note: this will use Feb 28th for a Feb 29th birthday in a non leap year.
        LocalDate relevantBirthday = birthday.PlusYears(ageThreeMonthsAgo + 1);

        // Find the strictly-previous Friday to start the discount interval
        LocalDate discountStart = relevantBirthday.With(DateAdjusters.Previous(IsoDayOfWeek.Friday));        
        LocalDate discountEndInclusive = discountStart.PlusDays(9);
        DateInterval discountInterval = new DateInterval(discountStart, discountEndInclusive);

        bool actualResult = discountInterval.Contains(today);
        Console.WriteLine($"{today} / {birthday} / {(actualResult == expectedResult ? "PASS" : "FAIL")}");
    }
}

答案 1 :(得分:2)

建议解决方案(使用JS和时间戳)

// Helper constants
const day = 1000 * 60 * 60 * 24
const friday = 5
const $input = document.getElementById('datepicker')
const $result = document.getElementById('result')
const $tests = document.getElementById('tests')

// Generate the period based on birthday and now (optional)
function getPeriod(birthday, today) {
  const now = new Date(today || new Date())
  // reset to start at 00:00
  now.setHours(0)
  now.setMinutes(0)
  now.setMilliseconds(0)

  // Available beggining date
  const begin = new Date(now - (10 * day))
  birthday = new Date(birthday)
  // fix birthday year
  birthday.setFullYear(begin.getFullYear())
  
  // if it already passed, jump to next year
  if (birthday < begin)
    birthday.setFullYear(birthday.getFullYear() + 1)

  // Start date
  const start = new Date(birthday)
  
  // if the birthday is already on friday, jump to previous friday (3th condition) 
  if(start.getDay() === friday)
    start.setTime(start.getTime() - (day * 7))

  // go to the last friday
  while (start.getDay() !== friday) 
    start.setTime(start.getTime() - day)
  // return found date + 10 days
  return [start, new Date(start.getTime() + (10 * day)-1)]
}


function calculatePeriod() {
  const birthday = $input.value
  const [begin, end] = getPeriod(birthday)
  $result.innerHTML = begin.toString() + '<br>' + end.toString()
}


const testCases = [
  ['February 15, 2019', 'February 22, 2000'],
  ['February 24, 2019', 'February 22, 2000'],
  ['February 25, 2019', 'February 22, 2000'],
  ['December 28, 2018', 'January 03, 2000'],
  ['December 27, 2018', 'January 03, 2000'],
  ['December 27, 2019', 'January 03, 2000'],
  ['January 01, 2019 ', 'January 03, 2000'],
  ['January 01, 2019 ', 'December 31, 2000'],
  ['January 01, 2019 ', 'December 28, 2000'],
]

testCases.map(([now, birthday]) => {
  const [begin, end] = getPeriod(birthday, now)
  $tests.innerHTML += `BIRTH: ${birthday}<br>NOW:   ${now}<br>BEGIN: ${begin}<br>END  : ${end}<br><br>`
})
<h3>Select an date</h3>
<input type="date" id="datepicker" value="2019-01-01"/>
<button onclick="calculatePeriod()">Calculate</button>

<p>Result: <pre id="result">...</pre></p>
<hr />

<h3>Tests</h3>
<pre id="tests"></pre>

答案 2 :(得分:1)

Jon Skeet提供了相同的解决方案,但在PHP中使用Carbon。

use Carbon\Carbon;

class UserBirthdayTest extends TestCase
{
    /**
     * Setup this test
     */
    public function setUp()
    {
        parent::setUp();
    }

    /**
     * Test create methods on basic CRUDs controllers
     */
    public function testCreate()
    {
        $this->validate("2019-02-15", "2000-02-22", true);
        $this->validate("2019-02-24", "2000-02-22", true);
        $this->validate("2019-02-25", "2000-02-22", false);
        $this->validate("2018-12-28", "2000-01-03", true);
        $this->validate("2019-01-01", "2000-01-03", true);
        $this->validate("2018-12-27", "2000-01-03", false);
        $this->validate("2019-12-27", "2000-01-03", true);
    }

    /**
     * @param \PHPUnit\Framework\TestResult $today
     * @param $birthday
     * @param $expectedResult
     * @return \PHPUnit\Framework\TestResult|void
     */
    public function validate($today, $birthday, $expectedResult)
    {
        $today = new Carbon($today);
        $birthday = new Carbon($birthday);

        // closest discount period
        $threeMonthsAgo = (new Carbon($today))->subMonth(3);

        $ageThreeMonthsAgo = $birthday->diffInYears($threeMonthsAgo);

        // Note: this will use Feb 28th for a Feb 29th birthday in a non leap year.
        $relevantBirthday = $birthday->addYears($ageThreeMonthsAgo + 1);

        // Find the strictly-previous Friday to start the discount interval
        $discountStart = Carbon::createFromTimestamp(strtotime('last friday', $relevantBirthday->timestamp));
        $discountEndInclusive = (new Carbon($discountStart))->addDays(9);

        $actualResult = $today->greaterThanOrEqualTo($discountStart) && $today->lessThanOrEqualTo($discountEndInclusive); // between($discountStart, $discountEndInclusive, false);

        $this->assertEquals($expectedResult, $actualResult);
    }
}