diff --git a/src/Validator/DateTime.php b/src/Validator/DateTime.php new file mode 100644 index 00000000..b9b9524a --- /dev/null +++ b/src/Validator/DateTime.php @@ -0,0 +1,296 @@ + "Invalid type given. String expected", + self::INVALID_DATETIME => "The input does not appear to be a valid datetime", + ); + + /** + * Optional locale + * + * @var string|null + */ + protected $locale; + + /** + * @var int + */ + protected $dateType = IntlDateFormatter::NONE; + + /** + * @var int + */ + protected $timeType = IntlDateFormatter::NONE; + + /** + * Optional timezone + * + * @var string + */ + protected $timezone; + + /** + * @var string + */ + protected $pattern; + + /** + * @var int + */ + protected $calendar = IntlDateFormatter::GREGORIAN; + + /** + * @var IntlDateFormatter + */ + protected $formatter; + + /** + * Is the formatter invalidated + * + * Invalidation occurs when immutable properties are changed + * + * @var bool + */ + protected $invalidateFormatter = false; + + /** + * Constructor for the Date validator + * + * @param array|Traversable $options + */ + public function __construct($options = array()) + { + parent::__construct($options); + + if (null === $this->locale) { + $this->locale = Locale::getDefault(); + } + if (null === $this->timezone) { + $this->timezone = date_default_timezone_get(); + } + } + + /** + * Sets the calendar to be used by the IntlDateFormatter + * + * @param integer|null $calendar + * @return DateTime provides fluent interface + */ + public function setCalendar($calendar) + { + $this->calendar = $calendar; + + return $this; + } + + /** + * Returns the calendar to by the IntlDateFormatter + * + * @return int + */ + public function getCalendar() + { + return ($this->formatter && !$this->invalidateFormatter) ? $this->getIntlDateFormatter()->getCalendar() : $this->calendar; + } + + /** + * Sets the date format to be used by the IntlDateFormatter + * + * @param integer|null $dateType + * @return DateTime provides fluent interface + */ + public function setDateType($dateType) + { + $this->dateType = $dateType; + $this->invalidateFormatter = true; + + return $this; + } + + /** + * Returns the date format used by the IntlDateFormatter + * + * @return int + */ + public function getDateType() + { + return $this->dateType; + } + + /** + * Sets the pattern to be used by the IntlDateFormatter + * + * @param string|null $pattern + * @return DateTime provides fluent interface + */ + public function setPattern($pattern) + { + $this->pattern = $pattern; + + return $this; + } + + /** + * Returns the pattern used by the IntlDateFormatter + * + * @return string + */ + public function getPattern() + { + return ($this->formatter && !$this->invalidateFormatter) ? $this->getIntlDateFormatter()->getPattern() : $this->pattern; + } + + /** + * Sets the time format to be used by the IntlDateFormatter + * + * @param integer|null $timeType + * @return DateTime provides fluent interface + */ + public function setTimeType($timeType) + { + $this->timeType = $timeType; + $this->invalidateFormatter = true; + + return $this; + } + + /** + * Returns the time format used by the IntlDateFormatter + * + * @return int + */ + public function getTimeType() + { + return $this->timeType; + } + + /** + * Sets the timezone to be used by the IntlDateFormatter + * + * @param string|null $timezone + * @return DateTime provides fluent interface + */ + public function setTimezone($timezone) + { + $this->timezone = $timezone; + + return $this; + } + + /** + * Returns the timezone used by the IntlDateFormatter or the system default if none given + * + * @return string + */ + public function getTimezone() + { + return ($this->formatter && !$this->invalidateFormatter) ? $this->getIntlDateFormatter()->getTimeZoneId() : $this->timezone; + } + + /** + * Sets the locale to be used by the IntlDateFormatter + * + * @param string|null $locale + * @return DateTime provides fluent interface + */ + public function setLocale($locale) + { + $this->locale = $locale; + $this->invalidateFormatter = true; + + return $this; + } + + /** + * Returns the locale used by the IntlDateFormatter or the system default if none given + * + * @return string + */ + public function getLocale() + { + return $this->locale; + } + + /** + * Returns true if and only if $value is a floating-point value + * + * @param string $value + * @return bool + * @throws Exception\InvalidArgumentException + */ + public function isValid($value) + { + if (!is_string($value)) { + $this->error(self::INVALID); + + return false; + } + + $this->setValue($value); + + $formatter = $this->getIntlDateFormatter(); + + if (intl_is_failure($formatter->getErrorCode())) { + throw new Exception\InvalidArgumentException("Invalid locale string given"); + } + + $position = 0; + $parsedDate = $formatter->parse($value, $position); + + if (intl_is_failure($formatter->getErrorCode())) { + $this->error(self::INVALID_DATETIME); + + return false; + } + + if ($position != strlen($value)) { + $this->error(self::INVALID_DATETIME); + + return false; + } + + return true; + } + + /** + * Returns a non lenient configured IntlDateFormatter + * + * @return IntlDateFormatter + */ + protected function getIntlDateFormatter() + { + if ($this->formatter == null || $this->invalidateFormatter) { + $this->formatter = new IntlDateFormatter($this->getLocale(), $this->getDateType(), $this->getTimeType(), + $this->getTimezone(), $this->getCalendar(), $this->getPattern()); + + $this->formatter->setLenient(false); + + $this->invalidateFormatter = false; + } + + return $this->formatter; + } +} diff --git a/test/Validator/DateTimeTest.php b/test/Validator/DateTimeTest.php new file mode 100644 index 00000000..14298c35 --- /dev/null +++ b/test/Validator/DateTimeTest.php @@ -0,0 +1,152 @@ +locale = Locale::getDefault(); + $this->timezone = date_default_timezone_get(); + + $this->validator = new DateTimeValidator(array('locale' => 'en', 'timezone'=>'Europe/Amsterdam')); + } + + public function tearDown() + { + Locale::setDefault($this->locale); + date_default_timezone_set($this->timezone); + } + + /** + * Ensures that the validator follows expected behavior + * + * @param string $value that will be tested + * @param boolean $expected expected result of assertion + * @param array $options fed into the validator before validation + * @dataProvider basicProvider name of method that provide the above parameters + * @return void + */ + public function testBasic($value, $expected, $options = array()) + { + $this->validator->setOptions($options); + + $this->assertEquals($expected, $this->validator->isValid($value), + 'Failed expecting ' . $value . ' being ' . ($expected ? 'true' : 'false') . + sprintf(" (locale:%s, dateType: %s, timeType: %s, pattern:%s)", $this->validator->getLocale(), + $this->validator->getDateType(), $this->validator->getTimeType(), $this->validator->getPattern())); + } + + public function basicProvider() + { + return array( + array('May 30, 2013', true, array('locale'=>'en', 'dateType' => \IntlDateFormatter::MEDIUM, 'timeType' => \IntlDateFormatter::NONE)), + array('30.Mai.2013', true, array('locale'=>'de', 'dateType' => \IntlDateFormatter::MEDIUM, 'timeType' => \IntlDateFormatter::NONE)), + array('30 Mei 2013', true, array('locale'=>'nl', 'dateType' => \IntlDateFormatter::MEDIUM, 'timeType' => \IntlDateFormatter::NONE)), + + array('May 38, 2013', false, array('locale'=>'en', 'dateType' => \IntlDateFormatter::FULL, 'timeType' => \IntlDateFormatter::NONE)), + array('Dienstag, 28. Mai 2013', true, array('locale'=>'de', 'dateType' => \IntlDateFormatter::FULL, 'timeType' => \IntlDateFormatter::NONE)), + array('Maandag 28 Mei 2013', true, array('locale'=>'nl', 'dateType' => \IntlDateFormatter::FULL, 'timeType' => \IntlDateFormatter::NONE)), + + array('0:00', true, array('locale'=>'nl', 'dateType' => \IntlDateFormatter::NONE, 'timeType' => \IntlDateFormatter::SHORT)), + array('01:01', true, array('locale'=>'nl', 'dateType' => \IntlDateFormatter::NONE, 'timeType' => \IntlDateFormatter::SHORT)), + array('01:01:01', true, array('locale'=>'nl', 'dateType' => \IntlDateFormatter::NONE, 'timeType' => \IntlDateFormatter::MEDIUM)), + array('01:01:01 +2', true, array('locale'=>'nl', 'dateType' => \IntlDateFormatter::NONE, 'timeType' => \IntlDateFormatter::LONG)), + array('03:30:42 am +2', true, array('locale'=>'en', 'dateType' => \IntlDateFormatter::NONE, 'timeType' => \IntlDateFormatter::LONG)), + ); + } + + /** + * Ensures that getMessages() returns expected default value + * + * @return void + */ + public function testGetMessages() + { + $this->assertEquals(array(), $this->validator->getMessages()); + } + + /** + * Ensures that set/getLocale() works + */ + public function testOptionLocale() + { + $this->validator->setLocale('de'); + $this->assertEquals('de', $this->validator->getLocale()); + } + + public function testApplicationOptionLocale() + { + Locale::setDefault('nl'); + $valid = new DateTimeValidator(); + $this->assertEquals(Locale::getDefault(), $valid->getLocale()); + } + + /** + * Ensures that set/getTimezone() works + */ + public function testOptionTimezone() + { + $this->validator->setLocale('Europe/Berlin'); + $this->assertEquals('Europe/Berlin', $this->validator->getLocale()); + } + + public function testApplicationOptionTimezone() + { + date_default_timezone_set('Europe/Berlin'); + $valid = new DateTimeValidator(); + $this->assertEquals(date_default_timezone_get(), $valid->getTimezone()); + } + + /** + * Ensures that an omitted pattern results in a calculated pattern by IntlDateFormatter + */ + public function testOptionPatternOmitted() + { + // null before validation + $this->assertNull($this->validator->getPattern()); + + $this->validator->isValid('does not matter'); + + // set after + $this->assertEquals('yyyyMMdd hh:mm a', $this->validator->getPattern()); + } + + /** + * Ensures that setting the pattern results in pattern used (by the validation process) + */ + public function testOptionPattern() + { + $this->validator->setOptions(array('pattern'=>'hh:mm')); + + $this->assertTrue($this->validator->isValid('02:00')); + $this->assertEquals('hh:mm', $this->validator->getPattern()); + } + +}