diff options
Diffstat (limited to 'www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Time.php')
-rw-r--r-- | www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Time.php | 609 |
1 files changed, 609 insertions, 0 deletions
diff --git a/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Time.php b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Time.php new file mode 100644 index 00000000..00a473d2 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/includes/dataitems/SMW_DI_Time.php @@ -0,0 +1,609 @@ +<?php + +use SMW\DataValues\Time\CalendarModel; +use SMW\DataValues\Time\JulianDay; +use SMW\Exception\DataItemException; + +/** + * This class implements time data items. + * Such data items represent a unique point in time, given in either Julian or + * Gregorian notation (possibly proleptic), and a precision setting that states + * which of the components year, month, day, time were specified expicitly. + * Even when not specified, the data item always assumes default values for the + * missing parts, so the item really captures one point in time, no intervals. + * Times are always assumed to be in UTC. + * + * "Y0K issue": Neither the Gregorian nor the Julian calendar assume a year 0, + * i.e. the year 1 BC(E) was followed by 1 AD/CE. See + * http://en.wikipedia.org/wiki/Year_zero + * This implementation adheres to this convention and disallows year 0. The + * stored year numbers use positive numbers for CE and negative numbers for + * BCE. This is not just relevant for the question of how many years have + * (exactly) passed since a given date, but also for the location of leap + * years. + * + * @since 1.6 + * + * @author Markus Krötzsch + * @ingroup SMWDataItems + */ +class SMWDITime extends SMWDataItem implements CalendarModel { + + const PREC_Y = SMW_PREC_Y; + const PREC_YM = SMW_PREC_YM; + const PREC_YMD = SMW_PREC_YMD; + const PREC_YMDT = SMW_PREC_YMDT; + + /** + * The year before which we do not accept anything but year numbers and + * largely discourage calendar models. + */ + const PREHISTORY = -10000; + + /** + * Maximal number of days in a given month. + * @var array + */ + protected static $m_daysofmonths = [ 1 => 31, 2 => 29, 3 => 31, 4 => 30, 5 => 31, 6 => 30, 7 => 31, 8 => 31, 9 => 30, 10 => 31, 11 => 30, 12 => 31 ]; + + /** + * Precision SMWDITime::PREC_Y, SMWDITime::PREC_YM, + * SMWDITime::PREC_YMD, or SMWDITime::PREC_YMDT. + * @var integer + */ + protected $m_precision; + /** + * Calendar model: SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN. + * @var integer + */ + protected $m_model; + /** + * Number of year, possibly negative. + * @var integer + */ + protected $m_year; + /** + * Number of month. + * @var integer + */ + protected $m_month; + /** + * Number of day. + * @var integer + */ + protected $m_day; + /** + * Hours of the day. + * @var integer + */ + protected $m_hours; + /** + * Minutes of the hour. + * @var integer + */ + protected $m_minutes; + /** + * Seconds of the minute. + * @var integer + */ + protected $m_seconds; + + /** + * @var integer + */ + protected $timezone; + + /** + * @var integer|null + */ + protected $era = null; + + /** + * @var integer + */ + protected $julianDay = null; + + /** + * Create a time data item. All time components other than the year can + * be false to indicate that they are not specified. This will affect + * the internal precision setting. The missing values are initialised + * to minimal values (0 or 1) for internal calculations. + * + * @param $calendarmodel integer one of SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN + * @param $year integer number of the year (possibly negative) + * @param $month mixed integer number or false + * @param $day mixed integer number or false + * @param $hour mixed integer number or false + * @param $minute mixed integer number or false + * @param $second mixed integer number or false + * @param integer|false $timezone + * + * @todo Implement more validation here. + */ + public function __construct( $calendarmodel, $year, $month = false, $day = false, + $hour = false, $minute = false, $second = false, $timezone = false ) { + + if ( ( $calendarmodel != self::CM_GREGORIAN ) && ( $calendarmodel != self::CM_JULIAN ) ) { + throw new DataItemException( "Unsupported calendar model constant \"$calendarmodel\"." ); + } + + if ( $year == 0 ) { + throw new DataItemException( "There is no year 0 in Gregorian and Julian calendars." ); + } + + $this->m_model = $calendarmodel; + $this->m_year = intval( $year ); + $this->m_month = $month != false ? intval( $month ) : 1; + $this->m_day = $day != false ? intval( $day ) : 1; + $this->m_hours = $hour !== false ? intval( $hour ) : 0; + $this->m_minutes = $minute !== false ? intval( $minute ) : 0; + $this->m_seconds = $second !== false ? floatval( $second ) : 0; + + $this->timezone = $timezone !== false ? $timezone : 0; + $year = strval( $year ); + $this->era = $year{0} === '+' ? 1 : ( $year{0} === '-' ? -1 : 0 ); + + if ( $this->isOutOfBoundsBySome() ) { + throw new DataItemException( "Part of the date is out of bounds." ); + } + + if ( $this->isOutOfBoundsByDayNumberOfMonth() ) { + throw new DataItemException( "Month {$this->m_month} in year {$this->m_year} did not have {$this->m_day} days in this calendar model." ); + } + + $this->setPrecisionLevelBy( $month, $day, $hour ); + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getDIType() { + return SMWDataItem::TYPE_TIME; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getCalendarModel() { + return $this->m_model; + } + + /** + * @since 2.5 + * + * @return integer + */ + public function getTimezone() { + return $this->timezone; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getPrecision() { + return $this->m_precision; + } + + /** + * Indicates whether a user explicitly used an era marker even for a positive + * year. + * + * - [-1] indicates BC(E) + * - [0]/null indicates no era marker + * - [1] indicates AD/CE was used + * + * @since 2.4 + * + * @return integer + */ + public function getEra() { + return $this->era; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getYear() { + return $this->m_year; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getMonth() { + return $this->m_month; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getDay() { + return $this->m_day; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getHour() { + return $this->m_hours; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getMinute() { + return $this->m_minutes; + } + + /** + * @since 1.6 + * + * @return integer + */ + public function getSecond() { + return $this->m_seconds; + } + + /** + * @since 2.4 + * + * @return string + */ + public function getCalendarModelLiteral() { + + $literal = [ + self::CM_GREGORIAN => '', + self::CM_JULIAN => 'JL' + ]; + + return $literal[$this->m_model]; + } + + /** + * @since 2.4 + * + * @param DateTime $dateTime + * + * @return SMWDITime|false + */ + public static function newFromDateTime( DateTime $dateTime ) { + + $calendarModel = self::CM_JULIAN; + + $year = $dateTime->format( 'Y' ); + $month = $dateTime->format( 'm' ); + $day = $dateTime->format( 'd' ); + + if ( ( $year > 1582 ) || + ( ( $year == 1582 ) && ( $month > 10 ) ) || + ( ( $year == 1582 ) && ( $month == 10 ) && ( $day > 4 ) ) ) { + $calendarModel = self::CM_GREGORIAN; + } + + return self::doUnserialize( $calendarModel . '/' . $dateTime->format( 'Y/m/d/H/i/s.u' ) ); + } + + /** + * @since 2.4 + * + * @return DateTime + */ + public function asDateTime() { + + $year = str_pad( $this->m_year, 4, '0', STR_PAD_LEFT ); + + // Avoid "Failed to parse time string (-900-02-02 00:00:00) at + // position 7 (-): Double timezone specification" + if ( $this->m_year < 0 ) { + $year = '-' . str_pad( $this->m_year * -1, 4, '0', STR_PAD_LEFT ); + } + + // Avoid "Failed to parse time string (1300-11-02 12:03:25.888499949) at + // at position 11 (1): The timezone could not ..." + $seconds = number_format( str_pad( $this->m_seconds, 2, '0', STR_PAD_LEFT ), 7, '.', '' ); + + $time = $year . '-' . + str_pad( $this->m_month, 2, '0', STR_PAD_LEFT ) . '-' . + str_pad( $this->m_day, 2, '0', STR_PAD_LEFT ) . ' ' . + str_pad( $this->m_hours, 2, '0', STR_PAD_LEFT ) . ':' . + str_pad( $this->m_minutes, 2, '0', STR_PAD_LEFT ) . ':' . + $seconds; + + return new DateTime( $time ); + } + + /** + * Creates and returns a new instance of SMWDITime from a MW timestamp. + * + * @since 1.8 + * + * @param string $timestamp must be in format + * + * @return SMWDITime|false + */ + public static function newFromTimestamp( $timestamp ) { + $timestamp = wfTimestamp( TS_MW, (string)$timestamp ); + + if ( $timestamp === false ) { + return false; + } + + return new self( + self::CM_GREGORIAN, + substr( $timestamp, 0, 4 ), + substr( $timestamp, 4, 2 ), + substr( $timestamp, 6, 2 ), + substr( $timestamp, 8, 2 ), + substr( $timestamp, 10, 2 ), + substr( $timestamp, 12, 2 ) + ); + } + + /** + * Returns a MW timestamp representation of the value. + * + * @since 1.6.2 + * + * @param $outputtype + * + * @return mixed + */ + public function getMwTimestamp( $outputtype = TS_UNIX ) { + return wfTimestamp( + $outputtype, + implode( '', [ + str_pad( $this->m_year, 4, '0', STR_PAD_LEFT ), + str_pad( $this->m_month, 2, '0', STR_PAD_LEFT ), + str_pad( $this->m_day, 2, '0', STR_PAD_LEFT ), + str_pad( $this->m_hours, 2, '0', STR_PAD_LEFT ), + str_pad( $this->m_minutes, 2, '0', STR_PAD_LEFT ), + str_pad( $this->m_seconds, 2, '0', STR_PAD_LEFT ), + ] ) + ); + } + + /** + * Get the data in the specified calendar model. This might require + * conversion. + * @note Conversion can be unreliable for very large absolute year + * numbers when the internal calculations hit floating point accuracy. + * Callers might want to avoid this (calendar models make little sense + * in such cases anyway). + * @param $calendarmodel integer one of SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN + * @return SMWDITime + */ + public function getForCalendarModel( $calendarmodel ) { + if ( $calendarmodel == $this->m_model ) { + return $this; + } else { + return self::newFromJD( $this->getJD(), $calendarmodel, $this->m_precision ); + } + } + + /** + * Return a number that helps comparing time data items. For + * dates in the Julian Day era (roughly from 4713 BCE onwards), we use + * the Julian Day number. For earlier dates, the (negative) year number + * with a fraction for the date is used (times are ignored). This + * avoids calculation errors that would occur for very ancient dates + * if the JD number was used there. + * @return double sortkey + */ + public function getSortKey() { + $jd = ( $this->m_year >= -4713 ) ? $jd = $this->getJD() : -1; + if ( $jd > 0 ) { + return $jd; + } else { + return $this->m_year - 1 + ( $this->m_month - 1 ) / 12 + ( $this->m_day - 1 ) / 12 / 31; + } + } + + /** + * @since 1.6 + * + * @return double + */ + public function getJD() { + + if ( $this->julianDay !== null ) { + return $this->julianDay; + } + + $this->julianDay = JulianDay::getJD( + $this->getCalendarModel(), + $this->getYear(), + $this->getMonth(), + $this->getDay(), + $this->getHour(), + $this->getMinute(), + $this->getSecond() + ); + + return $this->julianDay; + } + + /** + * @since 1.6 + * + * @return string + */ + public function getSerialization() { + $result = strval( $this->m_model ) . '/' . ( $this->era > 0 ? '+' : '' ) . strval( $this->m_year ); + + if ( $this->m_precision >= self::PREC_YM ) { + $result .= '/' . strval( $this->m_month ); + } + + if ( $this->m_precision >= self::PREC_YMD ) { + $result .= '/' . strval( $this->m_day ); + } + + if ( $this->m_precision >= self::PREC_YMDT ) { + $result .= '/' . strval( $this->m_hours ) . '/' . strval( $this->m_minutes ) . '/' . strval( $this->m_seconds ) . '/' . strval( $this->timezone ); + } + + return $result; + } + + /** + * Create a data item from the provided serialization string. + * + * @return SMWDITime + */ + public static function doUnserialize( $serialization ) { + $parts = explode( '/', $serialization, 8 ); + $values = []; + + if ( count( $parts ) <= 1 ) { + throw new DataItemException( "Unserialization failed: the string \"$serialization\" is no valid URI." ); + } + + for ( $i = 0; $i < 8; $i += 1 ) { + + $values[$i] = false; + + // Can contain something like '1/1970/1/12/11/43/0/Asia/Tokyo' + if ( $i == 7 && isset( $parts[$i] ) ) { + $values[$i] = strval( $parts[$i] ); + continue; + } + + if ( $i < count( $parts ) ) { + + if ( $parts[$i] !== '' && !is_numeric( $parts[$i] ) ) { + throw new DataItemException( "Unserialization failed: the string \"$serialization\" is no valid datetime specification." ); + } + + // 6 == seconds, we want to keep microseconds + $values[$i] = $i == 6 ? floatval( $parts[$i] ) : intval( $parts[$i] ); + + // Find out whether the input contained an explicit AD/CE era marker + if ( $i == 1 ) { + $values[$i] = ( $parts[1]{0} === '+' ? '+' : '' ) . $values[$i]; + } + } + } + + return new self( $values[0], $values[1], $values[2], $values[3], $values[4], $values[5], $values[6], $values[7] ); + } + + /** + * Create a new time dataItem from a specified Julian Day number, + * calendar model, presicion. + * + * @param double $jdValue + * @param integer|null $calendarmodel + * @param integer|null $precision + * + * @return DITime object + */ + public static function newFromJD( $jdValue, $calendarModel = null, $precision = null, $timezone = false ) { + + $hour = $minute = $second = false; + $year = $month = $day = false; + $jdValue = JulianDay::format( $jdValue ); + + if ( $precision === null ) { + $precision = strpos( strval( $jdValue ), '.5' ) !== false ? self::PREC_YMD : self::PREC_YMDT; + } + + list( $calendarModel, $year, $month, $day ) = JulianDay::JD2Date( $jdValue, $calendarModel ); + + if ( $precision <= self::PREC_YM ) { + $day = false; + if ( $precision === self::PREC_Y ) { + $month = false; + } + } + + if ( $precision === self::PREC_YMDT ) { + list( $hour, $minute, $second ) = JulianDay::JD2Time( $jdValue ); + } + + return new self( $calendarModel, $year, $month, $day, $hour, $minute, $second, $timezone ); + } + + /** + * Find out whether the given year number is a leap year. + * This calculation assumes that neither calendar has a year 0. + * @param $year integer year number + * @param $calendarmodel integer either SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN + * @return boolean + */ + static public function isLeapYear( $year, $calendarmodel ) { + $astroyear = ( $year < 1 ) ? ( $year + 1 ) : $year; + if ( $calendarmodel == self::CM_JULIAN ) { + return ( $astroyear % 4 ) == 0; + } else { + return ( ( $astroyear % 400 ) == 0 ) || + ( ( ( $astroyear % 4 ) == 0 ) && ( ( $astroyear % 100 ) != 0 ) ); + } + } + + /** + * Find out how many days the given month had in the given year + * based on the specified calendar model. + * This calculation assumes that neither calendar has a year 0. + * @param $month integer month number + * @param $year integer year number + * @param $calendarmodel integer either SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN + * @return boolean + */ + static public function getDayNumberForMonth( $month, $year, $calendarmodel ) { + if ( $month !== 2 ) { + return self::$m_daysofmonths[$month]; + } elseif ( self::isLeapYear( $year, $calendarmodel ) ) { + return 29; + } else { + return 28; + } + } + + public function equals( SMWDataItem $di ) { + if ( $di->getDIType() !== SMWDataItem::TYPE_TIME ) { + return false; + } + + return $di->getSortKey() === $this->getSortKey(); + } + + private function isOutOfBoundsBySome() { + return ( $this->m_hours < 0 ) || ( $this->m_hours > 23 ) || + ( $this->m_minutes < 0 ) || ( $this->m_minutes > 59 ) || + ( $this->m_seconds < 0 ) || ( $this->m_seconds > 59 ) || + ( $this->m_month < 1 ) || ( $this->m_month > 12 ); + } + + private function isOutOfBoundsByDayNumberOfMonth() { + return $this->m_day > self::getDayNumberForMonth( $this->m_month, $this->m_year, $this->m_model ); + } + + private function setPrecisionLevelBy( $month, $day, $hour ) { + if ( $month === false ) { + $this->m_precision = self::PREC_Y; + } elseif ( $day === false ) { + $this->m_precision = self::PREC_YM; + } elseif ( $hour === false ) { + $this->m_precision = self::PREC_YMD; + } else { + $this->m_precision = self::PREC_YMDT; + } + } + +} |