venerdì 10 marzo 2017

Sacchi di monete... Perl!


C'è una baggianata che gira ultimamente su WhatsApp relativamente a mesi nei quali avvengono coincidenze stranissime: 5 domeniche, 5 sabati, ecc.
L'evento viene citato come rarissimo, tanto che si verificherebbe solo 823 anni (o qualcosa di simile).
Ovviamente si tratta del solito spam mirato solo a collezionare informazioni e numeri di telefono.


Ma sarà vero?
Basta scrivere qualche riga Perl per sapere quali sono i mesi nei quali sono presenti un certo numero di ripetizioni dello stesso giorno della settimana.
Ecco quindi la mia soluzione:



use v5.20;
use DateTime;
my $now = DateTime->now();

for ( 1..823 ){
    $now->set( day => 1, month => $_ )
    &&
    say "Anno " . $now->year . " mese " . $now->month
    . " ha 5 " . join ', ', qw( Lun Mar Mer Gio Ven Sab Dom )
    [ map {  $_ , ( $_ + 1 ) % 7 , ( $_ + 2 ) % 7  } ($now->day_of_week - 1 ) ]
        for ( grep { ( $_ % 2 == 0 && $_ > 7 ) || ( $_ % 2 == 1 && $_ <= 7 ) } ( 1..12 ) );

    $now->add( years => 1 );
}


L'idea è semplice: anzitutto si filtrano i mesi con 31 giorni, funzione svolta dall'operatore grep che estrae i numeri di mese 1,3,5,7,8,10,12.
Questi vengono usati per impostare il mese corrente, si seleziona il primo giorno e si mappa il day_of_week in una lista di tre valori: la posizione del primo
giorno della settimana e dei due successivi adiacenti (modulo 7, ovviamente per consentire il "giro"). Il tutto viene poi stampato.
Il trucco è che il ciclo è postfisso, quindi va letto da destra a sinistra; inoltre l'inizializzazione del mese corrente è in "and" logico alla stampa
del valore, fatto che consente di usare un solo ciclo.
Per andare avanti di un anno, beh, basta aggiungere un anno e reiterare.
L'output è semplice:


Anno 2838 mese 3 ha 5 Lun, Mar, Mer
Anno 2838 mese 5 ha 5 Sab, Dom, Lun
Anno 2838 mese 7 ha 5 Gio, Ven, Sab
Anno 2838 mese 8 ha 5 Dom, Lun, Mar

Questa soluzione si basa sul semplice fatto che tutti i mesi con 31 giorni hanno sempre 3 giorni consecutivi con ripetizione pari a 5.
Ma se non fosse noto ciò?
Beh, complicandosi la vita si può fare una soluzione general-purpose:


use v5.10;
use DateTime;

my ( $count , $count_per_month, $years, $now, $dow, $ldom, $cons )
    = ( 5, 3, 823,
        DateTime->now()->set( day => 1, month => 1 ),
        sub{ qw( Lun Mart Merc Giov Ven Sab Dom )[ $_[ 0 ] ] },
        sub{ $_[ 0 ]->month != $_[ 0 ]->clone->add( days => 1 )->month },
        sub{ $_[ 0 ] == $_[ 1 ] - 1 && $_[ 1 ] == $_[ 2 ] - 1 } ) ;

for ( 1..( 365 * $years ) ) {
    my $me = \%{ $days_of->{ $now->year }->{ $now->month } };
    ++ $me->{ $now->day_of_week - 1 };

    say "Anno " . $now->year . " mese " . $now->month . " => "
        . join ', ',
        map { $me->{ $_ } . " " . $dow->( $_ )  }
          sort keys %$me
        if ( $ldom->( $now )
             && ( grep { $_ >= $count } values %$me )
             >= $count_per_month
             && $cons->( ( grep { $me->{ $_ } >= $count }  sort keys %$me )  ) );


    $now->add( days => 1 );
}


Il funzionamento è abbastanza semplice, anche se un po' compatto.
Anzitutto mi affido a DateTime che fornisce una interfaccia ad oggetti per la gestione delle date.
Si inizializzano una serie di variabili:

  • $count rappresenta quante ripetizioni dello stesso giorno della settimana sto cercando (5);
  • $countpermonth rappresenta quante ripetizioni di $count ci devono essere in un mese. Ad esempio impostandolo al valore 3 significa
    cercare almeno 3 giorni in un mese che si ripetano 5 volte (ossia il mese deve avere, ad esempio, almeno 5 sabati, 5 domeniche e 5 lunedì);
  • $years su quanti anni da quello corrente si vuole iterare;
  • $now la data corrente, impostata all'inizio dell'anno corrente;
  • $dow è una routine di comodo che ritorna il nome simbolico del giorno della settimana (day of week);
  • $ldom altra funzione di utilità che controlla se l'oggetto DateTime corrente rappresenta l'ultimo giorno della settimana. In particolare si controlla
    se un DateTime successivo di un giorno ha un mese differente;
  • $cons è una funzione che controlla che gli indici siano consecutivi fra loro (es 1,2,3), anche se ci vorrebbe la logica modulo 7 per
    intercettare il giro di boa alla domenica…

Da qui in poi si itera in un ciclo di 365 iterazioni per anno, salvando in un hash a piu' livelli il conteggio dei giorni della settimana di un mese.
L'hash assume una struttura dove al primo livello si trova l'anno corrente, poi il mese, poi il giorno della settimana, poi il conteggio di tale giorno.
Ad esempio una ipotetica iterazione potrebbe produrre:


(
 2017 => 1 => ( Lun => 3,
                Mart => 3,
                Mer  => 4,
                Gio  => 5,
                Ven => 3
                Sab => 3,
                Dom => 3
);
La variabile $me serve solo a tenere il codice un po' piu' compatto, ossia ad accedere alla parte dell'anno/mese corrente
nel'hash globale.
Anzitutto si incrementa il contatore del giorno della settimana (già tradotto da numerico a mnemonico con $dow).
Segue un blocco say..if, quindi è opportuno partire prima dalla condizione if: se il giorno è di fine mese e si ha un conteggio di valori di singoli
giorni della settimana superiore a quello cercato si procede alla stampa. Questo consente di valutare se stampare il mese corrente solo se si è
iterato su tutti i giorni del mese e solo se si sono trovate almeni $count_per_month ripetizioni di giorni con conteggio di $count.
La parte di say include un map preso un prestito da un idioma ben noto, quindi va letto da destra a sinistra:

  1. si ordinano le chiavi (nomi mnemonici dei giorni della settimana) dell'hash del mese corrente;
  2. si crea una stringa con il conteggio di ripetizioni del giorno specificato e del suo nome mnemonico (parte map);
  3. si effettua un join di tutte le stringhe così costruite.

E quindi si stampa il tutto, con risultato analogo a quello del caso precedente.


Nessun commento: