laminas / laminas-form

Validate and display simple and complex forms, casting forms to business objects and vice versa
https://docs.laminas.dev/laminas-form/
BSD 3-Clause "New" or "Revised" License
80 stars 52 forks source link

keys(option-values) generation in get(Hours|Minutes|Seconds|...)Options #221

Closed pine3ree closed 1 year ago

pine3ree commented 1 year ago

I have a doubt about using IntlDateFormatter for options keys generations in Date//DateTime Select view helpers. The patterns used are:

'MM' // for months
'dd' // for days
'HH' // for hours
'mm' // for minutes
'ss' // for seconds

At first sight one would think that all thos values are zerofilled integer strings, which could be fetched via a simple:

sprintf('%02d', $value);  // also faster, no formatter instance needed

But testing it with various locales we get non numeric symbols. Example:

$datetime = new DateTimeImmutable();

$locales = IntlCalendar::getAvailableLocales();

echo "\n";

foreach ($locales as $locale) {
    $dateFormatter = new IntlDateFormatter(
        $locale,
        IntlDateFormatter::LONG,
        IntlDateFormatter::LONG,
        null,
        null,
        'MM dd HH mm ss' // Combining all the patterns used in the options builders 
    );

    $formattedDate = $dateFormatter->format($datetime);

    if (!preg_match('/^[a-zA-Z0-9\:\-\s]+$/', $formattedDate)) {
        echo "$locale => " . $dateFormatter->format($datetime) . "\n";
    }
}

/*
RESULTS
ar => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_001 => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_BH => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_DJ => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_EG => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_ER => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_IL => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_IQ => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_JO => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_KM => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_KW => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_LB => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_MR => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_OM => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_PS => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_QA => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_SA => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_SD => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_SO => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_SS => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_SY => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_TD => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ar_YE => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
as => ০৬ ০৯ ১৯ ৫৭ ৩৪
as_IN => ০৬ ০৯ ১৯ ৫৭ ৩৪
bn => ০৬ ০৯ ১৯ ৫৭ ৩৪
bn_BD => ০৬ ০৯ ১৯ ৫৭ ৩৪
bn_IN => ০৬ ০৯ ১৯ ৫৭ ৩৪
ccp => 𑄶𑄼 𑄶𑄿 𑄷𑄿 𑄻𑄽 𑄹𑄺
ccp_BD => 𑄶𑄼 𑄶𑄿 𑄷𑄿 𑄻𑄽 𑄹𑄺
ccp_IN => 𑄶𑄼 𑄶𑄿 𑄷𑄿 𑄻𑄽 𑄹𑄺
ckb => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ckb_IQ => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ckb_IR => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
dz => ༠༦ ༠༩ ༡༩ ༥༧ ༣༤
dz_BT => ༠༦ ༠༩ ༡༩ ༥༧ ༣༤
fa => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
fa_AF => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
fa_IR => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
ff_Adlm => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ff_Adlm_BF => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ff_Adlm_CM => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ff_Adlm_GH => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ff_Adlm_GM => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ff_Adlm_GN => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ff_Adlm_GW => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ff_Adlm_LR => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ff_Adlm_MR => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ff_Adlm_NE => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ff_Adlm_NG => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ff_Adlm_SL => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ff_Adlm_SN => 𞥐𞥖 𞥐𞥙 𞥑𞥙 𞥕𞥗 𞥓𞥔
ks => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
ks_Arab => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
ks_Arab_IN => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
lrc => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
lrc_IQ => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
lrc_IR => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
mni => ০৬ ০৯ ১৯ ৫৭ ৩৪
mni_Beng => ০৬ ০৯ ১৯ ৫৭ ৩৪
mni_Beng_IN => ০৬ ০৯ ১৯ ৫৭ ৩৪
mr => ०६ ०९ १९ ५७ ३४
mr_IN => ०६ ०९ १९ ५७ ३४
my => ၀၆ ၀၉ ၁၉ ၅၇ ၃၄
my_MM => ၀၆ ၀၉ ၁၉ ၅၇ ၃၄
mzn => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
mzn_IR => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
ne => ०६ ०९ १९ ५७ ३४
ne_IN => ०६ ०९ १९ ५७ ३४
ne_NP => ०६ ०९ १९ ५७ ३४
pa_Arab => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
pa_Arab_PK => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
ps => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
ps_AF => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
ps_PK => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
sa => ०६ ०९ १९ ५७ ३४
sa_IN => ०६ ०९ १९ ५७ ३४
sat => ᱐᱖ ᱐᱙ ᱑᱙ ᱕᱗ ᱓᱔
sat_Olck => ᱐᱖ ᱐᱙ ᱑᱙ ᱕᱗ ᱓᱔
sat_Olck_IN => ᱐᱖ ᱐᱙ ᱑᱙ ᱕᱗ ᱓᱔
sd => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
sd_Arab => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
sd_Arab_PK => ٠٦ ٠٩ ١٩ ٥٧ ٣٤
ur_IN => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
uz_Arab => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
uz_Arab_AF => ۰۶ ۰۹ ۱۹ ۵۷ ۳۴
* /

but in DateTimeSelect element ,for instance, setValue() is using DateTimeSelect::format() which is not (multi-)locale aware, so it always output zerofilled int string. So setting a value after a form submission with errors will not set the posted values into the select components.

Shouldn't we use sprintf for keys ( (select-option values)) and intl-date-formatter for values (select-option labels)? ...or am I totally missing something very obvious?

kind regards.

Slamdunk commented 1 year ago

I didn't know about date formatting resulting in non-numbers :open_mouth:

I'd say you have more knowledge about this topic than me: may you open a PR with related new tests, and see if all other tests still pass?

pine3ree commented 1 year ago

@Slamdunk I didn't know, or suspected, either (been using php since 2001 :smile: ). I always assumed that hours, minutes and seconds formatting options could only result in non-zerofilled and zerofilled int strings. At first I thought that the IntlDateFormatter was being used just for symmetry with the label (array value) formatter, to have a more elegant/consistent code. Curiosity made me try it with all avaiable locales... I haven't used selects for date/time inputs in years, but while creating a Plates extensions for laminas-form, I had to examine laminas-form-view-helpers related code more thoroughly. There is also another inconsistency. The year select element has both numeric keys (select-option-values) and values (select-option-labels). It should have intl-formatted values values.

There is only one test checking the formatted selects in es_CL. We would need tests checking that all the <option value=""> parts only contain 2 or 4 digits strings and no other chars, allowing the presented label to be locale-formatted

<?php

// INSTALL libs
// $ composer require laminas/laminas-form
// $ composer require laminas/laminas-i18n
// $ composer require laminas/laminas-view

use Laminas\Form\ConfigProvider;
use Laminas\Form\Element\DateTimeSelect;
use Laminas\Form\View\Helper\FormDateTimeSelect;
use Laminas\View\Renderer\PhpRenderer;

$locale = 'ar';

//Locale::setDefault($locale);
//setlocale(LC_ALL, $locale);

ini_set('display_errors', 'true');

$__dir = __DIR__;

require "{$__dir}/vendor/autoload.php";

$view                = new PhpRenderer();
$helperPluginManager = $view->getHelperPluginManager();
$viewHelperConfig    = (new ConfigProvider())->getViewHelperConfig();
$helperPluginManager->configure($viewHelperConfig);
$view->setHelperPluginManager($helperPluginManager);

$formDateTimeSelect = new FormDateTimeSelect();
$formDateTimeSelect->setView($view);

$datetimeField = new DateTimeSelect('createdAt');

$html = $formDateTimeSelect(
    $datetimeField,
    IntlDateFormatter::LONG,
    IntlDateFormatter::LONG,
    $locale
);

die("\n{$html}\n");
pine3ree commented 1 year ago

Hello @Slamdunk ,

should I prepare a pull-request for this? kind regards

PS We can use a simple sprintf or , since we already have a DateTimeInterface instance, just use the non-localized formatter:

Example for days select-option

    protected function getDaysOptions($pattern, string $locale, int $dateType): array
    {
        $dateFormatter = new IntlDateFormatter($locale, $dateType, IntlDateFormatter::NONE, null, null, $pattern);
        $date          = new DateTime('1970-01-01');

        $result = [];
        for ($day = 1; $day <= 31; $day++) {
            $key   = sprintf('%02d', $day); // 1
            $key   = $date->format('d'); // 2
            $value = $dateFormatter->format($date->getTimestamp());
            $result[$key] = $value;

            $date->modify('+1 day');
        }

        return $result;
    }