3
votes

Rechercher si une date se situe sur une plage de deux week-ends entre une autre date

Je crée une application qui donne des réductions aux utilisateurs et l'une des réductions est accordée le jour de son anniversaire. Comme il est vraiment difficile d'utiliser une réduction uniquement le jour de votre anniversaire, nous décidons de vous offrir la réduction toute la semaine, y compris les deux week-ends.

Quelques règles

  • Compte tenu de la date de naissance, la réduction débutera le vendredi précédent de l'anniversaire;
  • La réduction prendra fin 10 jours après le début, le dimanche;
  • Si l'anniversaire est un vendredi, obtenez toujours le dernier vendredi avant celui-ci;

Ok, nous avons donc besoin d'une méthode qui reçoive le birthdayDate et le todayDate et renvoie vrai ou faux indiquant si la date est sur la plage d'anniversaire.

Le problème

Tout va bien jusqu'à ce que vous commenciez à regarder les dates proches du changement d'année.
Si votre anniversaire est le 31 décembre 2018, vous pouvez utiliser la réduction le 6 janvier 2019, de la même manière, si votre anniversaire est le 1er janvier 2019, le 28 décembre 2018, vous pouvez déjà utiliser la réduction.
Il ne suffit donc pas de regarder uniquement l'anniversaire de l'année en cours et, comme les jours de la semaine changent chaque année, le dernier vendredi de votre anniversaire cette année ne sera pas le même jour le prochain.

Là est un moyen élégant de trouver facilement cette gamme et de créer la méthode retournant vrai ou faux?

Le vrai problème est de trouver quel anniversaire est l'anniversaire pertinent, car vous pouvez bénéficier de la réduction après votre dernier anniversaire et avant votre prochain anniversaire.

Le code d'origine est en PHP, mais comme il s'agit d'un problème d'algorithme, je peux accepter des réponses dans n'importe quelle langue

Cas de test

| 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  |


6 commentaires

Il semble que ce soit simplement "Trouvez le vendredi précédent avant l'anniversaire concerné". C'est assez facile à faire dans de nombreuses langues. Pouvez-vous montrer ce que vous avez déjà essayé? Je ne vois pas pourquoi les dates proches du changement d'année seraient en fait particulièrement pertinentes ici. S'il s'agit de savoir quel anniversaire est l'anniversaire pertinent, il serait utile que vous soyez plus précis à ce sujet. Ce serait formidable si vous pouviez lister toutes les entrées (par exemple, date actuelle, anniversaire) et la sortie attendue.


Trouver le dernier vendredi est vraiment simple, et la plupart des langues ont déjà une méthode pour cela. Le problème est: de quel anniversaire avez-vous besoin pour trouver le dernier vendredi? Peut-être que votre prochain anniversaire vous offre déjà la réduction. C'est peut-être le dernier. J'ajouterai d'autres cas à la question.


Il est difficile de savoir ce que vous entendez par «de quel anniversaire vous avez besoin» lorsque nous ne savons pas quel est le résultat attendu. Essayez-vous de savoir si la remise doit être valable aujourd'hui ? Ou une période pour montrer un utilisateur? Si vous pouviez épingler cela dans une langue spécifique avec un exemple reproductible minimal qui montre ce que vous avez essayé jusqu'à présent , il serait plus facile de vous aider.


J'ai ajouté quelques cas de test. Je révise le texte pour le rendre plus clair.


D'accord, ces cas de test facilitent définitivement l'aide. J'écrirai une réponse en utilisant C # comme langage d'implémentation, mais j'expliquerai ce qu'il fait aussi.


(Les années bissextiles seront probablement un peu pénibles, d'ailleurs.)


3 Réponses :


4
votes

Voici du code C # utilisant ma bibliothèque Noda Time qui passe tous les tests que vous avez spécifiés.

Le l'idée de base est simple:

  • Retournez dans trois mois à partir d'aujourd'hui
  • Rechercher l'anniversaire après cette date
  • Construisez la période de remise autour de cet anniversaire
  • Vérifiez si la date du jour se situe dans cette période

Le choix de trois mois est quelque peu arbitraire; il est simplement destiné à contourner le cas où vous venez d'avoir un anniversaire. (Il est possible que plus de 10 jours et moins de 355 jours convienne. Je trouve simplement 3 mois plus faciles à raisonner.)

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")}");
    }
}


0 commentaires

2
votes

Solution de proposition (utilisant JS et horodatages)

<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>
// 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>`
})


0 commentaires

1
votes

Voici la même solution de Jon Skeet, mais en PHP avec 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);
    }
}


0 commentaires