date_api.module 88 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940
  1. <?php
  2. /**
  3. * @file
  4. * This module will make the date API available to other modules.
  5. * Designed to provide a light but flexible assortment of functions
  6. * and constants, with more functionality in additional files that
  7. * are not loaded unless other modules specifically include them.
  8. */
  9. /**
  10. * Set up some constants.
  11. *
  12. * Includes standard date types, format strings, strict regex strings for ISO
  13. * and DATETIME formats (seconds are optional).
  14. *
  15. * The loose regex will find any variety of ISO date and time, with or
  16. * without time, with or without dashes and colons separating the elements,
  17. * and with either a 'T' or a space separating date and time.
  18. */
  19. define('DATE_ISO', 'date');
  20. define('DATE_UNIX', 'datestamp');
  21. define('DATE_DATETIME', 'datetime');
  22. define('DATE_ARRAY', 'array');
  23. define('DATE_OBJECT', 'object');
  24. define('DATE_ICAL', 'ical');
  25. define('DATE_FORMAT_ISO', "Y-m-d\TH:i:s");
  26. define('DATE_FORMAT_UNIX', "U");
  27. define('DATE_FORMAT_DATETIME', "Y-m-d H:i:s");
  28. define('DATE_FORMAT_ICAL', "Ymd\THis");
  29. define('DATE_FORMAT_ICAL_DATE', "Ymd");
  30. define('DATE_FORMAT_DATE', 'Y-m-d');
  31. define('DATE_REGEX_ISO', '/(\d{4})?(-(\d{2}))?(-(\d{2}))?([T\s](\d{2}))?(:(\d{2}))?(:(\d{2}))?/');
  32. define('DATE_REGEX_DATETIME', '/(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2}):?(\d{2})?/');
  33. define('DATE_REGEX_LOOSE', '/(\d{4})-?(\d{1,2})-?(\d{1,2})([T\s]?(\d{2}):?(\d{2}):?(\d{2})?(\.\d+)?(Z|[\+\-]\d{2}:?\d{2})?)?/');
  34. define('DATE_REGEX_ICAL_DATE', '/(\d{4})(\d{2})(\d{2})/');
  35. define('DATE_REGEX_ICAL_DATETIME', '/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?/');
  36. /**
  37. * Core DateTime extension module used for as many date operations as possible.
  38. */
  39. /**
  40. * Implements hook_help().
  41. */
  42. function date_help($path, $arg) {
  43. switch ($path) {
  44. case 'admin/help#date':
  45. $output = '';
  46. $messages = date_api_status();
  47. $output = '<h2>Date API Status</h2>';
  48. if (!empty($messages['success'])) {
  49. $output .= '<ul><li>' . implode('</li><li>', $messages['success']) . '</li></ul>';
  50. }
  51. if (!empty($messages['errors'])) {
  52. $output .= '<h3>Errors</h3><ul class="error"><li>' . implode('</li><li>', $messages['errors']) . '</li></ul>';
  53. }
  54. if (module_exists('date_tools')) {
  55. $output .= '<h3>Date Tools</h3>' . t('Dates and calendars can be complicated to set up. The !date_wizard makes it easy to create a simple date content type and with a date field.', array('!date_wizard' => l(t('Date wizard'), 'admin/config/date/tools/date_wizard')));
  56. }
  57. else {
  58. $output .= '<h3>Date Tools</h3>' . t('Dates and calendars can be complicated to set up. If you enable the Date Tools module, it provides a Date Wizard that makes it easy to create a simple date content type with a date field.');
  59. }
  60. $output .= '<h2>More Information</h2><p>' . t('Complete documentation for the Date and Date API modules is available at <a href="@link">http://drupal.org/node/92460</a>.', array('@link' => 'http://drupal.org/node/262062')) . '</p>';
  61. return $output;
  62. }
  63. }
  64. /**
  65. * Helper function to retun the status of required date variables.
  66. */
  67. function date_api_status() {
  68. $t = get_t();
  69. $error_messages = array();
  70. $success_messages = array();
  71. $value = variable_get('date_default_timezone');
  72. if (isset($value)) {
  73. $success_messages[] = $t('The timezone has been set to <a href="@regional_settings">@timezone</a>.', array('@regional_settings' => url('admin/config/regional/settings'), '@timezone' => $value));
  74. }
  75. else {
  76. $error_messages[] = $t('The Date API requires that you set up the <a href="@regional_settings">site timezone</a> to function correctly.', array('@regional_settings' => url('admin/config/regional/settings')));
  77. }
  78. $value = variable_get('date_first_day');
  79. if (isset($value)) {
  80. $days = date_week_days();
  81. $success_messages[] = $t('The first day of the week has been set to <a href="@regional_settings">@day</a>.', array('@regional_settings' => url('admin/config/regional/settings'), '@day' => $days[$value]));
  82. }
  83. else {
  84. $error_messages[] = $t('The Date API requires that you set up the <a href="@regional_settings">site first day of week settings</a> to function correctly.', array('@regional_settings' => url('admin/config/regional/settings')));
  85. }
  86. $value = variable_get('date_format_medium');
  87. if (isset($value)) {
  88. $now = date_now();
  89. $success_messages[] = $t('The medium date format type has been set to @value. You may find it helpful to add new format types like Date, Time, Month, or Year, with appropriate formats, at <a href="@regional_date_time">Date and time</a> settings.', array('@value' => $now->format($value), '@regional_date_time' => url('admin/config/regional/date-time')));
  90. }
  91. else {
  92. $error_messages[] = $t('The Date API requires that you set up the <a href="@regional_date_time">system date formats</a> to function correctly.', array('@regional_date_time' => url('admin/config/regional/date-time')));
  93. }
  94. return array('errors', $error_messages, 'success' => $success_messages);
  95. }
  96. /**
  97. * Implements hook_menu().
  98. *
  99. * Creates a 'Date API' section on the administration page for Date
  100. * modules to use for their configuration and settings.
  101. */
  102. function date_api_menu() {
  103. $items['admin/config/date'] = array(
  104. 'title' => 'Date API',
  105. 'description' => 'Settings for modules the use the Date API.',
  106. 'position' => 'left',
  107. 'weight' => -10,
  108. 'page callback' => 'system_admin_menu_block_page',
  109. 'access arguments' => array('administer site configuration'),
  110. 'file' => 'system.admin.inc',
  111. 'file path' => drupal_get_path('module', 'system'),
  112. );
  113. return $items;
  114. }
  115. /**
  116. * Extend PHP DateTime class with granularity handling, merge functionality and
  117. * slightly more flexible initialization parameters.
  118. *
  119. * This class is a Drupal independent extension of the >= PHP 5.2 DateTime
  120. * class.
  121. *
  122. * @see FeedsDateTimeElement class
  123. */
  124. class DateObject extends DateTime {
  125. public $granularity = array();
  126. public $errors = array();
  127. protected static $allgranularity = array(
  128. 'year',
  129. 'month',
  130. 'day',
  131. 'hour',
  132. 'minute',
  133. 'second',
  134. 'timezone'
  135. );
  136. private $serializedTime;
  137. private $serializedTimezone;
  138. /**
  139. * Prepares the object during serialization.
  140. *
  141. * We are extending a core class and core classes cannot be serialized.
  142. *
  143. * @return array
  144. * Returns an array with the names of the variables that were serialized.
  145. *
  146. * @see http://bugs.php.net/41334
  147. * @see http://bugs.php.net/39821
  148. */
  149. public function __sleep() {
  150. $this->serializedTime = $this->format('c');
  151. $this->serializedTimezone = $this->getTimezone()->getName();
  152. return array('serializedTime', 'serializedTimezone');
  153. }
  154. /**
  155. * Re-builds the object using local variables.
  156. */
  157. public function __wakeup() {
  158. $this->__construct($this->serializedTime, new DateTimeZone($this->serializedTimezone));
  159. }
  160. /**
  161. * Returns the date object as a string.
  162. *
  163. * @return string
  164. * The date object formatted as a string.
  165. */
  166. public function __toString() {
  167. return $this->format(DATE_FORMAT_DATETIME) . ' ' . $this->getTimeZone()->getName();
  168. }
  169. /**
  170. * Constructs a date object.
  171. *
  172. * @param string $time
  173. * A date/time string or array. Defaults to 'now'.
  174. * @param object|string|null $tz
  175. * PHP DateTimeZone object, string or NULL allowed. Defaults to NULL.
  176. * @param string $format
  177. * PHP date() type format for parsing. Doesn't support timezones; if you
  178. * have a timezone, send NULL and the default constructor method will
  179. * hopefully parse it. $format is recommended in order to use negative or
  180. * large years, which php's parser fails on.
  181. */
  182. public function __construct($time = 'now', $tz = NULL, $format = NULL) {
  183. $this->timeOnly = FALSE;
  184. $this->dateOnly = FALSE;
  185. // Store the raw time input so it is available for validation.
  186. $this->originalTime = $time;
  187. // Allow string timezones.
  188. if (!empty($tz) && !is_object($tz)) {
  189. $tz = new DateTimeZone($tz);
  190. }
  191. // Default to the site timezone when not explicitly provided.
  192. elseif (empty($tz)) {
  193. $tz = date_default_timezone_object();
  194. }
  195. // Special handling for Unix timestamps expressed in the local timezone.
  196. // Create a date object in UTC and convert it to the local timezone. Don't
  197. // try to turn things like '2010' with a format of 'Y' into a timestamp.
  198. if (is_numeric($time) && (empty($format) || $format == 'U')) {
  199. // Assume timestamp.
  200. $time = "@" . $time;
  201. $date = new DateObject($time, 'UTC');
  202. if ($tz->getName() != 'UTC') {
  203. $date->setTimezone($tz);
  204. }
  205. $time = $date->format(DATE_FORMAT_DATETIME);
  206. $format = DATE_FORMAT_DATETIME;
  207. $this->addGranularity('timezone');
  208. }
  209. elseif (is_array($time)) {
  210. // Assume we were passed an indexed array.
  211. if (empty($time['year']) && empty($time['month']) && empty($time['day'])) {
  212. $this->timeOnly = TRUE;
  213. }
  214. if (empty($time['hour']) && empty($time['minute']) && empty($time['second'])) {
  215. $this->dateOnly = TRUE;
  216. }
  217. $this->errors = $this->arrayErrors($time);
  218. // Make this into an ISO date, forcing a full ISO date even if some values
  219. // are missing.
  220. $time = $this->toISO($time, TRUE);
  221. // We checked for errors already, skip parsing the input values.
  222. $format = NULL;
  223. }
  224. else {
  225. // Make sure dates like 2010-00-00T00:00:00 get converted to
  226. // 2010-01-01T00:00:00 before creating a date object
  227. // to avoid unintended changes in the month or day.
  228. $time = date_make_iso_valid($time);
  229. }
  230. // The parse function will also set errors on the date parts.
  231. if (!empty($format)) {
  232. $arg = self::$allgranularity;
  233. $element = array_pop($arg);
  234. while (!$this->parse($time, $tz, $format) && $element != 'year') {
  235. $element = array_pop($arg);
  236. $format = date_limit_format($format, $arg);
  237. }
  238. if ($element == 'year') {
  239. return FALSE;
  240. }
  241. }
  242. elseif (is_string($time)) {
  243. // PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
  244. $time = str_replace("GMT-", "-", $time);
  245. $time = str_replace("GMT+", "+", $time);
  246. // We are going to let the parent dateObject do a best effort attempt to
  247. // turn this string into a valid date. It might fail and we want to
  248. // control the error messages.
  249. try {
  250. @parent::__construct($time, $tz);
  251. }
  252. catch (Exception $e) {
  253. $this->errors['date'] = $e;
  254. return;
  255. }
  256. if (empty($this->granularity)) {
  257. $this->setGranularityFromTime($time, $tz);
  258. }
  259. }
  260. // If we haven't got a valid timezone name yet, we need to set one or
  261. // we will get undefined index errors.
  262. // This can happen if $time had an offset or no timezone.
  263. if (!$this->getTimezone() || !preg_match('/[a-zA-Z]/', $this->getTimezone()->getName())) {
  264. // If the original $tz has a name, use it.
  265. if (preg_match('/[a-zA-Z]/', $tz->getName())) {
  266. $this->setTimezone($tz);
  267. }
  268. // We have no information about the timezone so must fallback to a default.
  269. else {
  270. $this->setTimezone(new DateTimeZone("UTC"));
  271. $this->errors['timezone'] = t('No valid timezone name was provided.');
  272. }
  273. }
  274. }
  275. /**
  276. * Merges two date objects together using the current date values as defaults.
  277. *
  278. * @param object $other
  279. * Another date object to merge with.
  280. *
  281. * @return object
  282. * A merged date object.
  283. */
  284. public function merge(FeedsDateTime $other) {
  285. $other_tz = $other->getTimezone();
  286. $this_tz = $this->getTimezone();
  287. // Figure out which timezone to use for combination.
  288. $use_tz = ($this->hasGranularity('timezone') || !$other->hasGranularity('timezone')) ? $this_tz : $other_tz;
  289. $this2 = clone $this;
  290. $this2->setTimezone($use_tz);
  291. $other->setTimezone($use_tz);
  292. $val = $this2->toArray(TRUE);
  293. $otherval = $other->toArray();
  294. foreach (self::$allgranularity as $g) {
  295. if ($other->hasGranularity($g) && !$this2->hasGranularity($g)) {
  296. // The other class has a property we don't; steal it.
  297. $this2->addGranularity($g);
  298. $val[$g] = $otherval[$g];
  299. }
  300. }
  301. $other->setTimezone($other_tz);
  302. $this2->setDate($val['year'], $val['month'], $val['day']);
  303. $this2->setTime($val['hour'], $val['minute'], $val['second']);
  304. return $this2;
  305. }
  306. /**
  307. * Sets the time zone for the current date.
  308. *
  309. * Overrides default DateTime function. Only changes output values if
  310. * actually had time granularity. This should be used as a "converter" for
  311. * output, to switch tzs.
  312. *
  313. * In order to set a timezone for a datetime that doesn't have such
  314. * granularity, merge() it with one that does.
  315. *
  316. * @param object $tz
  317. * A timezone object.
  318. * @param bool $force
  319. * Whether or not to skip a date with no time. Defaults to FALSE.
  320. */
  321. public function setTimezone($tz, $force = FALSE) {
  322. // PHP 5.2.6 has a fatal error when setting a date's timezone to itself.
  323. // http://bugs.php.net/bug.php?id=45038
  324. if (version_compare(PHP_VERSION, '5.2.7', '<') && $tz == $this->getTimezone()) {
  325. $tz = new DateTimeZone($tz->getName());
  326. }
  327. if (!$this->hasTime() || !$this->hasGranularity('timezone') || $force) {
  328. // This has no time or timezone granularity, so timezone doesn't mean
  329. // much. We set the timezone using the method, which will change the
  330. // day/hour, but then we switch back.
  331. $arr = $this->toArray(TRUE);
  332. parent::setTimezone($tz);
  333. $this->setDate($arr['year'], $arr['month'], $arr['day']);
  334. $this->setTime($arr['hour'], $arr['minute'], $arr['second']);
  335. $this->addGranularity('timezone');
  336. return;
  337. }
  338. return parent::setTimezone($tz);
  339. }
  340. /**
  341. * Returns date formatted according to given format.
  342. *
  343. * Overrides base format function, formats this date according to its
  344. * available granularity, unless $force'ed not to limit to granularity.
  345. *
  346. * @TODO Add translation into this so translated names will be provided.
  347. *
  348. * @param string $format
  349. * A date format string.
  350. * @param bool $force
  351. * Whether or not to limit the granularity. Defaults to FALSE.
  352. *
  353. * @return string|false
  354. * Returns the formatted date string on success or FALSE on failure.
  355. */
  356. public function format($format, $force = FALSE) {
  357. return parent::format($force ? $format : date_limit_format($format, $this->granularity));
  358. }
  359. /**
  360. * Adds a granularity entry to the array.
  361. *
  362. * @param string $g
  363. * A single date part.
  364. */
  365. public function addGranularity($g) {
  366. $this->granularity[] = $g;
  367. $this->granularity = array_unique($this->granularity);
  368. }
  369. /**
  370. * Removes a granularity entry from the array.
  371. *
  372. * @param string $g
  373. * A single date part.
  374. */
  375. public function removeGranularity($g) {
  376. if (($key = array_search($g, $this->granularity)) !== FALSE) {
  377. unset($this->granularity[$key]);
  378. }
  379. }
  380. /**
  381. * Checks granularity array for a given entry.
  382. *
  383. * @param array|null $g
  384. * An array of date parts. Defaults to NULL.
  385. *
  386. * @returns bool
  387. * TRUE if the date part is present in the date's granularity.
  388. */
  389. public function hasGranularity($g = NULL) {
  390. if ($g === NULL) {
  391. // Just want to know if it has something valid means no lower
  392. // granularities without higher ones.
  393. $last = TRUE;
  394. foreach (self::$allgranularity as $arg) {
  395. if ($arg == 'timezone') {
  396. continue;
  397. }
  398. if (in_array($arg, $this->granularity) && !$last) {
  399. return FALSE;
  400. }
  401. $last = in_array($arg, $this->granularity);
  402. }
  403. return in_array('year', $this->granularity);
  404. }
  405. if (is_array($g)) {
  406. foreach ($g as $gran) {
  407. if (!in_array($gran, $this->granularity)) {
  408. return FALSE;
  409. }
  410. }
  411. return TRUE;
  412. }
  413. return in_array($g, $this->granularity);
  414. }
  415. /**
  416. * Determines if a a date is valid for a given granularity.
  417. *
  418. * @param array|null $granularity
  419. * An array of date parts. Defaults to NULL.
  420. * @param bool $flexible
  421. * TRUE if the granuliarty is flexible, FALSE otherwise. Defaults to FALSE.
  422. *
  423. * @return bool
  424. * Whether a date is valid for a given granularity.
  425. */
  426. public function validGranularity($granularity = NULL, $flexible = FALSE) {
  427. $true = $this->hasGranularity() && (!$granularity || $flexible || $this->hasGranularity($granularity));
  428. if (!$true && $granularity) {
  429. foreach ((array) $granularity as $part) {
  430. if (!$this->hasGranularity($part) && in_array($part, array(
  431. 'second',
  432. 'minute',
  433. 'hour',
  434. 'day',
  435. 'month',
  436. 'year')
  437. )) {
  438. switch ($part) {
  439. case 'second':
  440. $this->errors[$part] = t('The second is missing.');
  441. break;
  442. case 'minute':
  443. $this->errors[$part] = t('The minute is missing.');
  444. break;
  445. case 'hour':
  446. $this->errors[$part] = t('The hour is missing.');
  447. break;
  448. case 'day':
  449. $this->errors[$part] = t('The day is missing.');
  450. break;
  451. case 'month':
  452. $this->errors[$part] = t('The month is missing.');
  453. break;
  454. case 'year':
  455. $this->errors[$part] = t('The year is missing.');
  456. break;
  457. }
  458. }
  459. }
  460. }
  461. return $true;
  462. }
  463. /**
  464. * Returns whether this object has time set.
  465. *
  466. * Used primarily for timezone conversion and formatting.
  467. *
  468. * @return bool
  469. * TRUE if the date contains time parts, FALSE otherwise.
  470. */
  471. public function hasTime() {
  472. return $this->hasGranularity('hour');
  473. }
  474. /**
  475. * Returns whether the input values included a year.
  476. *
  477. * Useful to use pseudo date objects when we only are interested in the time.
  478. *
  479. * @todo $this->completeDate does not actually exist?
  480. */
  481. public function completeDate() {
  482. return $this->completeDate;
  483. }
  484. /**
  485. * Removes unwanted date parts from a date.
  486. *
  487. * In common usage we should not unset timezone through this.
  488. *
  489. * @param array $granularity
  490. * An array of date parts.
  491. */
  492. public function limitGranularity($granularity) {
  493. foreach ($this->granularity as $key => $val) {
  494. if ($val != 'timezone' && !in_array($val, $granularity)) {
  495. unset($this->granularity[$key]);
  496. }
  497. }
  498. }
  499. /**
  500. * Determines the granularity of a date based on the constructor's arguments.
  501. *
  502. * @param string $time
  503. * A date string.
  504. * @param bool $tz
  505. * TRUE if the date has a timezone, FALSE otherwise.
  506. */
  507. protected function setGranularityFromTime($time, $tz) {
  508. $this->granularity = array();
  509. $temp = date_parse($time);
  510. // Special case for 'now'.
  511. if ($time == 'now') {
  512. $this->granularity = array(
  513. 'year',
  514. 'month',
  515. 'day',
  516. 'hour',
  517. 'minute',
  518. 'second',
  519. );
  520. }
  521. else {
  522. // This PHP date_parse() method currently doesn't have resolution down to
  523. // seconds, so if there is some time, all will be set.
  524. foreach (self::$allgranularity as $g) {
  525. if ((isset($temp[$g]) && is_numeric($temp[$g])) || ($g == 'timezone' && (isset($temp['zone_type']) && $temp['zone_type'] > 0))) {
  526. $this->granularity[] = $g;
  527. }
  528. }
  529. }
  530. if ($tz) {
  531. $this->addGranularity('timezone');
  532. }
  533. }
  534. /**
  535. * Converts a date string into a date object.
  536. *
  537. * @param string $date
  538. * The date string to parse.
  539. * @param object $tz
  540. * A timezone object.
  541. * @param string $format
  542. * The date format string.
  543. *
  544. * @return object
  545. * Returns the date object.
  546. */
  547. protected function parse($date, $tz, $format) {
  548. $array = date_format_patterns();
  549. foreach ($array as $key => $value) {
  550. // The letter with no preceding '\'.
  551. $patterns[] = "`(^|[^\\\\\\\\])" . $key . "`";
  552. // A single character.
  553. $repl1[] = '${1}(.)';
  554. // The.
  555. $repl2[] = '${1}(' . $value . ')';
  556. }
  557. $patterns[] = "`\\\\\\\\([" . implode(array_keys($array)) . "])`";
  558. $repl1[] = '${1}';
  559. $repl2[] = '${1}';
  560. $format_regexp = preg_quote($format);
  561. // Extract letters.
  562. $regex1 = preg_replace($patterns, $repl1, $format_regexp, 1);
  563. $regex1 = str_replace('A', '(.)', $regex1);
  564. $regex1 = str_replace('a', '(.)', $regex1);
  565. preg_match('`^' . $regex1 . '$`', stripslashes($format), $letters);
  566. array_shift($letters);
  567. // Extract values.
  568. $regex2 = preg_replace($patterns, $repl2, $format_regexp, 1);
  569. $regex2 = str_replace('A', '(AM|PM)', $regex2);
  570. $regex2 = str_replace('a', '(am|pm)', $regex2);
  571. preg_match('`^' . $regex2 . '$`u', $date, $values);
  572. array_shift($values);
  573. // If we did not find all the values for the patterns in the format, abort.
  574. if (count($letters) != count($values)) {
  575. $this->errors['invalid'] = t('The value @date does not match the expected format.', array('@date' => $date));
  576. return FALSE;
  577. }
  578. $this->granularity = array();
  579. $final_date = array(
  580. 'hour' => 0,
  581. 'minute' => 0,
  582. 'second' => 0,
  583. 'month' => 1,
  584. 'day' => 1,
  585. 'year' => 0,
  586. );
  587. foreach ($letters as $i => $letter) {
  588. $value = $values[$i];
  589. switch ($letter) {
  590. case 'd':
  591. case 'j':
  592. $final_date['day'] = intval($value);
  593. $this->addGranularity('day');
  594. break;
  595. case 'n':
  596. case 'm':
  597. $final_date['month'] = intval($value);
  598. $this->addGranularity('month');
  599. break;
  600. case 'F':
  601. $array_month_long = array_flip(date_month_names());
  602. $final_date['month'] = array_key_exists($value, $array_month_long) ? $array_month_long[$value] : -1;
  603. $this->addGranularity('month');
  604. break;
  605. case 'M':
  606. $array_month = array_flip(date_month_names_abbr());
  607. $final_date['month'] = array_key_exists($value, $array_month) ? $array_month[$value] : -1;
  608. $this->addGranularity('month');
  609. break;
  610. case 'Y':
  611. $final_date['year'] = $value;
  612. $this->addGranularity('year');
  613. if (strlen($value) < 4) {
  614. $this->errors['year'] = t('The year is invalid. Please check that entry includes four digits.');
  615. }
  616. break;
  617. case 'y':
  618. $year = $value;
  619. // If no century, we add the current one ("06" => "2006").
  620. $final_date['year'] = str_pad($year, 4, substr(date("Y"), 0, 2), STR_PAD_LEFT);
  621. $this->addGranularity('year');
  622. break;
  623. case 'a':
  624. case 'A':
  625. $ampm = strtolower($value);
  626. break;
  627. case 'g':
  628. case 'h':
  629. case 'G':
  630. case 'H':
  631. $final_date['hour'] = intval($value);
  632. $this->addGranularity('hour');
  633. break;
  634. case 'i':
  635. $final_date['minute'] = intval($value);
  636. $this->addGranularity('minute');
  637. break;
  638. case 's':
  639. $final_date['second'] = intval($value);
  640. $this->addGranularity('second');
  641. break;
  642. case 'U':
  643. parent::__construct($value, $tz ? $tz : new DateTimeZone("UTC"));
  644. $this->addGranularity('year');
  645. $this->addGranularity('month');
  646. $this->addGranularity('day');
  647. $this->addGranularity('hour');
  648. $this->addGranularity('minute');
  649. $this->addGranularity('second');
  650. return $this;
  651. }
  652. }
  653. if (isset($ampm) && $ampm == 'pm' && $final_date['hour'] < 12) {
  654. $final_date['hour'] += 12;
  655. }
  656. elseif (isset($ampm) && $ampm == 'am' && $final_date['hour'] == 12) {
  657. $final_date['hour'] -= 12;
  658. }
  659. // Blank becomes current time, given TZ.
  660. parent::__construct('', $tz ? $tz : new DateTimeZone("UTC"));
  661. if ($tz) {
  662. $this->addGranularity('timezone');
  663. }
  664. // SetDate expects an integer value for the year, results can be unexpected
  665. // if we feed it something like '0100' or '0000'.
  666. $final_date['year'] = intval($final_date['year']);
  667. $this->errors += $this->arrayErrors($final_date);
  668. $granularity = drupal_map_assoc($this->granularity);
  669. // If the input value is '0000-00-00', PHP's date class will later
  670. // incorrectly convert it to something like '-0001-11-30' if we do setDate()
  671. // here. If we don't do setDate() here, it will default to the current date
  672. // and we will lose any way to tell that there was no date in the orignal
  673. // input values. So set a flag we can use later to tell that this date
  674. // object was created using only time values, and that the date values are
  675. // artifical.
  676. if (empty($final_date['year']) && empty($final_date['month']) && empty($final_date['day'])) {
  677. $this->timeOnly = TRUE;
  678. }
  679. elseif (empty($this->errors)) {
  680. // setDate() expects a valid year, month, and day.
  681. // Set some defaults for dates that don't use this to
  682. // keep PHP from interpreting it as the last day of
  683. // the previous month or last month of the previous year.
  684. if (empty($granularity['month'])) {
  685. $final_date['month'] = 1;
  686. }
  687. if (empty($granularity['day'])) {
  688. $final_date['day'] = 1;
  689. }
  690. $this->setDate($final_date['year'], $final_date['month'], $final_date['day']);
  691. }
  692. if (!isset($final_date['hour']) && !isset($final_date['minute']) && !isset($final_date['second'])) {
  693. $this->dateOnly = TRUE;
  694. }
  695. elseif (empty($this->errors)) {
  696. $this->setTime($final_date['hour'], $final_date['minute'], $final_date['second']);
  697. }
  698. return $this;
  699. }
  700. /**
  701. * Returns all standard date parts in an array.
  702. *
  703. * Will return '' for parts in which it lacks granularity.
  704. *
  705. * @param bool $force
  706. * Whether or not to limit the granularity. Defaults to FALSE.
  707. *
  708. * @return array
  709. * An array of formatted date part values, keyed by date parts.
  710. */
  711. public function toArray($force = FALSE) {
  712. return array(
  713. 'year' => $this->format('Y', $force),
  714. 'month' => $this->format('n', $force),
  715. 'day' => $this->format('j', $force),
  716. 'hour' => intval($this->format('H', $force)),
  717. 'minute' => intval($this->format('i', $force)),
  718. 'second' => intval($this->format('s', $force)),
  719. 'timezone' => $this->format('e', $force),
  720. );
  721. }
  722. /**
  723. * Creates an ISO date from an array of values.
  724. *
  725. * @param array $arr
  726. * An array of date values keyed by date part.
  727. * @param bool $full
  728. * (optional) Whether to force a full date by filling in missing values.
  729. * Defaults to FALSE.
  730. */
  731. public function toISO($arr, $full = FALSE) {
  732. // Add empty values to avoid errors. The empty values must create a valid
  733. // date or we will get date slippage, i.e. a value of 2011-00-00 will get
  734. // interpreted as November of 2010 by PHP.
  735. if ($full) {
  736. $arr += array(
  737. 'year' => 0,
  738. 'month' => 1,
  739. 'day' => 1,
  740. 'hour' => 0,
  741. 'minute' => 0,
  742. 'second' => 0,
  743. );
  744. }
  745. else {
  746. $arr += array(
  747. 'year' => '',
  748. 'month' => '',
  749. 'day' => '',
  750. 'hour' => '',
  751. 'minute' => '',
  752. 'second' => '',
  753. );
  754. }
  755. $datetime = '';
  756. if ($arr['year'] !== '') {
  757. $datetime = date_pad(intval($arr['year']), 4);
  758. if ($full || $arr['month'] !== '') {
  759. $datetime .= '-' . date_pad(intval($arr['month']));
  760. if ($full || $arr['day'] !== '') {
  761. $datetime .= '-' . date_pad(intval($arr['day']));
  762. }
  763. }
  764. }
  765. if ($arr['hour'] !== '') {
  766. $datetime .= $datetime ? 'T' : '';
  767. $datetime .= date_pad(intval($arr['hour']));
  768. if ($full || $arr['minute'] !== '') {
  769. $datetime .= ':' . date_pad(intval($arr['minute']));
  770. if ($full || $arr['second'] !== '') {
  771. $datetime .= ':' . date_pad(intval($arr['second']));
  772. }
  773. }
  774. }
  775. return $datetime;
  776. }
  777. /**
  778. * Forces an incomplete date to be valid.
  779. *
  780. * E.g., add a valid year, month, and day if only the time has been defined.
  781. *
  782. * @param array|string $date
  783. * An array of date parts or a datetime string with values to be massaged
  784. * into a valid date object.
  785. * @param string $format
  786. * (optional) The format of the date. Defaults to NULL.
  787. * @param string $default
  788. * (optional) If the fallback should use the first value of the date part,
  789. * or the current value of the date part. Defaults to 'first'.
  790. */
  791. public function setFuzzyDate($date, $format = NULL, $default = 'first') {
  792. $timezone = $this->getTimeZone() ? $this->getTimeZone()->getName() : NULL;
  793. $comp = new DateObject($date, $timezone, $format);
  794. $arr = $comp->toArray(TRUE);
  795. foreach ($arr as $key => $value) {
  796. // Set to intval here and then test that it is still an integer.
  797. // Needed because sometimes valid integers come through as strings.
  798. $arr[$key] = $this->forceValid($key, intval($value), $default, $arr['month'], $arr['year']);
  799. }
  800. $this->setDate($arr['year'], $arr['month'], $arr['day']);
  801. $this->setTime($arr['hour'], $arr['minute'], $arr['second']);
  802. }
  803. /**
  804. * Converts a date part into something that will produce a valid date.
  805. *
  806. * @param string $part
  807. * The date part.
  808. * @param int $value
  809. * The date value for this part.
  810. * @param string $default
  811. * (optional) If the fallback should use the first value of the date part,
  812. * or the current value of the date part. Defaults to 'first'.
  813. * @param int $month
  814. * (optional) Used when the date part is less than 'month' to specify the
  815. * date. Defaults to NULL.
  816. * @param int $year
  817. * (optional) Used when the date part is less than 'year' to specify the
  818. * date. Defaults to NULL.
  819. *
  820. * @return int
  821. * A valid date value.
  822. */
  823. protected function forceValid($part, $value, $default = 'first', $month = NULL, $year = NULL) {
  824. $now = date_now();
  825. switch ($part) {
  826. case 'year':
  827. $fallback = $now->format('Y');
  828. return !is_int($value) || empty($value) || $value < variable_get('date_min_year', 1) || $value > variable_get('date_max_year', 4000) ? $fallback : $value;
  829. case 'month':
  830. $fallback = $default == 'first' ? 1 : $now->format('n');
  831. return !is_int($value) || empty($value) || $value <= 0 || $value > 12 ? $fallback : $value;
  832. case 'day':
  833. $fallback = $default == 'first' ? 1 : $now->format('j');
  834. $max_day = isset($year) && isset($month) ? date_days_in_month($year, $month) : 31;
  835. return !is_int($value) || empty($value) || $value <= 0 || $value > $max_day ? $fallback : $value;
  836. case 'hour':
  837. $fallback = $default == 'first' ? 0 : $now->format('G');
  838. return !is_int($value) || $value < 0 || $value > 23 ? $fallback : $value;
  839. case 'minute':
  840. $fallback = $default == 'first' ? 0 : $now->format('i');
  841. return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value;
  842. case 'second':
  843. $fallback = $default == 'first' ? 0 : $now->format('s');
  844. return !is_int($value) || $value < 0 || $value > 59 ? $fallback : $value;
  845. }
  846. }
  847. /**
  848. * Finds possible errors in an array of date part values.
  849. *
  850. * The forceValid() function will change an invalid value to a valid one, so
  851. * we just need to see if the value got altered.
  852. *
  853. * @param array $arr
  854. * An array of date values, keyed by date part.
  855. *
  856. * @return array
  857. * An array of error messages, keyed by date part.
  858. */
  859. public function arrayErrors($arr) {
  860. $errors = array();
  861. $now = date_now();
  862. $default_month = !empty($arr['month']) ? $arr['month'] : $now->format('n');
  863. $default_year = !empty($arr['year']) ? $arr['year'] : $now->format('Y');
  864. $this->granularity = array();
  865. foreach ($arr as $part => $value) {
  866. // Explicitly set the granularity to the values in the input array.
  867. if (is_numeric($value)) {
  868. $this->addGranularity($part);
  869. }
  870. // Avoid false errors when a numeric value is input as a string by casting
  871. // as an integer.
  872. $value = intval($value);
  873. if (!empty($value) && $this->forceValid($part, $value, 'now', $default_month, $default_year) != $value) {
  874. // Use a switch/case to make translation easier by providing a different
  875. // message for each part.
  876. switch ($part) {
  877. case 'year':
  878. $errors['year'] = t('The year is invalid.');
  879. break;
  880. case 'month':
  881. $errors['month'] = t('The month is invalid.');
  882. break;
  883. case 'day':
  884. $errors['day'] = t('The day is invalid.');
  885. break;
  886. case 'hour':
  887. $errors['hour'] = t('The hour is invalid.');
  888. break;
  889. case 'minute':
  890. $errors['minute'] = t('The minute is invalid.');
  891. break;
  892. case 'second':
  893. $errors['second'] = t('The second is invalid.');
  894. break;
  895. }
  896. }
  897. }
  898. if ($this->hasTime()) {
  899. $this->addGranularity('timezone');
  900. }
  901. return $errors;
  902. }
  903. /**
  904. * Computes difference between two days using a given measure.
  905. *
  906. * @param object $date2_in
  907. * The stop date.
  908. * @param string $measure
  909. * (optional) A granularity date part. Defaults to 'seconds'.
  910. * @param bool $absolute
  911. * (optional) Indicate whether the absolute value of the difference should
  912. * be returned or if the sign should be retained. Defaults to TRUE.
  913. */
  914. public function difference($date2_in, $measure = 'seconds', $absolute = TRUE) {
  915. // Create cloned objects or original dates will be impacted by the
  916. // date_modify() operations done in this code.
  917. $date1 = clone($this);
  918. $date2 = clone($date2_in);
  919. if (is_object($date1) && is_object($date2)) {
  920. $diff = date_format($date2, 'U') - date_format($date1, 'U');
  921. if ($diff == 0) {
  922. return 0;
  923. }
  924. elseif ($diff < 0 && $absolute) {
  925. // Make sure $date1 is the smaller date.
  926. $temp = $date2;
  927. $date2 = $date1;
  928. $date1 = $temp;
  929. $diff = date_format($date2, 'U') - date_format($date1, 'U');
  930. }
  931. $year_diff = intval(date_format($date2, 'Y') - date_format($date1, 'Y'));
  932. switch ($measure) {
  933. // The easy cases first.
  934. case 'seconds':
  935. return $diff;
  936. case 'minutes':
  937. return $diff / 60;
  938. case 'hours':
  939. return $diff / 3600;
  940. case 'years':
  941. return $year_diff;
  942. case 'months':
  943. $format = 'n';
  944. $item1 = date_format($date1, $format);
  945. $item2 = date_format($date2, $format);
  946. if ($year_diff == 0) {
  947. return intval($item2 - $item1);
  948. }
  949. elseif ($year_diff < 0) {
  950. $item_diff = 0 - $item1;
  951. $item_diff -= intval((abs($year_diff) - 1) * 12);
  952. return $item_diff - (12 - $item2);
  953. }
  954. else {
  955. $item_diff = 12 - $item1;
  956. $item_diff += intval(($year_diff - 1) * 12);
  957. return $item_diff + $item2;
  958. }
  959. break;
  960. case 'days':
  961. $format = 'z';
  962. $item1 = date_format($date1, $format);
  963. $item2 = date_format($date2, $format);
  964. if ($year_diff == 0) {
  965. return intval($item2 - $item1);
  966. }
  967. elseif ($year_diff < 0) {
  968. $item_diff = 0 - $item1;
  969. for ($i = 1; $i < abs($year_diff); $i++) {
  970. date_modify($date1, '-1 year');
  971. $item_diff -= date_days_in_year($date1);
  972. }
  973. return $item_diff - (date_days_in_year($date2) - $item2);
  974. }
  975. else {
  976. $item_diff = date_days_in_year($date1) - $item1;
  977. for ($i = 1; $i < $year_diff; $i++) {
  978. date_modify($date1, '+1 year');
  979. $item_diff += date_days_in_year($date1);
  980. }
  981. return $item_diff + $item2;
  982. }
  983. break;
  984. case 'weeks':
  985. $week_diff = date_format($date2, 'W') - date_format($date1, 'W');
  986. $year_diff = date_format($date2, 'o') - date_format($date1, 'o');
  987. $sign = ($year_diff < 0) ? -1 : 1;
  988. for ($i = 1; $i <= abs($year_diff); $i++) {
  989. date_modify($date1, (($sign > 0) ? '+' : '-') . '1 year');
  990. $week_diff += (date_iso_weeks_in_year($date1) * $sign);
  991. }
  992. return $week_diff;
  993. }
  994. }
  995. return NULL;
  996. }
  997. }
  998. /**
  999. * Determines if the date element needs to be processed.
  1000. *
  1001. * Helper function to see if date element has been hidden by FAPI to see if it
  1002. * needs to be processed or just pass the value through. This is needed since
  1003. * normal date processing explands the date element into parts and then
  1004. * reconstructs it, which is not needed or desirable if the field is hidden.
  1005. *
  1006. * @param array $element
  1007. * The date element to check.
  1008. *
  1009. * @return bool
  1010. * TRUE if the element is effectively hidden, FALSE otherwise.
  1011. */
  1012. function date_hidden_element($element) {
  1013. // @TODO What else needs to be tested to see if dates are hidden or disabled?
  1014. if ((isset($element['#access']) && empty($element['#access']))
  1015. || !empty($element['#programmed'])
  1016. || in_array($element['#type'], array('hidden', 'value'))) {
  1017. return TRUE;
  1018. }
  1019. return FALSE;
  1020. }
  1021. /**
  1022. * Helper function for getting the format string for a date type.
  1023. *
  1024. * @param string $type
  1025. * A date type format name.
  1026. *
  1027. * @return string
  1028. * A date type format, like 'Y-m-d H:i:s'.
  1029. */
  1030. function date_type_format($type) {
  1031. switch ($type) {
  1032. case DATE_ISO:
  1033. return DATE_FORMAT_ISO;
  1034. case DATE_UNIX:
  1035. return DATE_FORMAT_UNIX;
  1036. case DATE_DATETIME:
  1037. return DATE_FORMAT_DATETIME;
  1038. case DATE_ICAL:
  1039. return DATE_FORMAT_ICAL;
  1040. }
  1041. }
  1042. /**
  1043. * Constructs an untranslated array of month names.
  1044. *
  1045. * Needed for CSS, translation functions, strtotime(), and other places
  1046. * that use the English versions of these words.
  1047. *
  1048. * @return array
  1049. * An array of month names.
  1050. */
  1051. function date_month_names_untranslated() {
  1052. static $month_names;
  1053. if (empty($month_names)) {
  1054. $month_names = array(
  1055. 1 => 'January',
  1056. 2 => 'February',
  1057. 3 => 'March',
  1058. 4 => 'April',
  1059. 5 => 'May',
  1060. 6 => 'June',
  1061. 7 => 'July',
  1062. 8 => 'August',
  1063. 9 => 'September',
  1064. 10 => 'October',
  1065. 11 => 'November',
  1066. 12 => 'December',
  1067. );
  1068. }
  1069. return $month_names;
  1070. }
  1071. /**
  1072. * Returns a translated array of month names.
  1073. *
  1074. * @param bool $required
  1075. * (optional) If FALSE, the returned array will include a blank value.
  1076. * Defaults to FALSE.
  1077. *
  1078. * @return array
  1079. * An array of month names.
  1080. */
  1081. function date_month_names($required = FALSE) {
  1082. $month_names = array();
  1083. foreach (date_month_names_untranslated() as $key => $month) {
  1084. $month_names[$key] = t($month, array(), array('context' => 'Long month name'));
  1085. }
  1086. $none = array('' => '');
  1087. return !$required ? $none + $month_names : $month_names;
  1088. }
  1089. /**
  1090. * Constructs a translated array of month name abbreviations.
  1091. *
  1092. * @param bool $required
  1093. * (optional) If FALSE, the returned array will include a blank value.
  1094. * Defaults to FALSE.
  1095. * @param int $length
  1096. * (optional) The length of the abbreviation. Defaults to 3.
  1097. *
  1098. * @return array
  1099. * An array of month abbreviations.
  1100. */
  1101. function date_month_names_abbr($required = FALSE, $length = 3) {
  1102. $month_names = array();
  1103. foreach (date_month_names_untranslated() as $key => $month) {
  1104. if ($length == 3) {
  1105. $month_names[$key] = t(substr($month, 0, $length), array());
  1106. }
  1107. else {
  1108. $month_names[$key] = t(substr($month, 0, $length), array(), array('context' => 'month_abbr'));
  1109. }
  1110. }
  1111. $none = array('' => '');
  1112. return !$required ? $none + $month_names : $month_names;
  1113. }
  1114. /**
  1115. * Constructs an untranslated array of week days.
  1116. *
  1117. * Needed for CSS, translation functions, strtotime(), and other places
  1118. * that use the English versions of these words.
  1119. *
  1120. * @param bool $refresh
  1121. * (optional) Whether to refresh the list. Defaults to TRUE.
  1122. *
  1123. * @return array
  1124. * An array of week day names
  1125. */
  1126. function date_week_days_untranslated($refresh = TRUE) {
  1127. static $weekdays;
  1128. if ($refresh || empty($weekdays)) {
  1129. $weekdays = array(
  1130. 'Sunday',
  1131. 'Monday',
  1132. 'Tuesday',
  1133. 'Wednesday',
  1134. 'Thursday',
  1135. 'Friday',
  1136. 'Saturday',
  1137. );
  1138. }
  1139. return $weekdays;
  1140. }
  1141. /**
  1142. * Returns a translated array of week names.
  1143. *
  1144. * @param bool $required
  1145. * (optional) If FALSE, the returned array will include a blank value.
  1146. * Defaults to FALSE.
  1147. *
  1148. * @return array
  1149. * An array of week day names
  1150. */
  1151. function date_week_days($required = FALSE, $refresh = TRUE) {
  1152. $weekdays = array();
  1153. foreach (date_week_days_untranslated() as $key => $day) {
  1154. $weekdays[$key] = t($day, array(), array('context' => ''));
  1155. }
  1156. $none = array('' => '');
  1157. return !$required ? $none + $weekdays : $weekdays;
  1158. }
  1159. /**
  1160. * Constructs a translated array of week day abbreviations.
  1161. *
  1162. * @param bool $required
  1163. * (optional) If FALSE, the returned array will include a blank value.
  1164. * Defaults to FALSE.
  1165. * @param bool $refresh
  1166. * (optional) Whether to refresh the list. Defaults to TRUE.
  1167. * @param int $length
  1168. * (optional) The length of the abbreviation. Defaults to 3.
  1169. *
  1170. * @return array
  1171. * An array of week day abbreviations
  1172. */
  1173. function date_week_days_abbr($required = FALSE, $refresh = TRUE, $length = 3) {
  1174. $weekdays = array();
  1175. switch ($length) {
  1176. case 1:
  1177. $context = 'day_abbr1';
  1178. break;
  1179. case 2:
  1180. $context = 'day_abbr2';
  1181. break;
  1182. default:
  1183. $context = '';
  1184. break;
  1185. }
  1186. foreach (date_week_days_untranslated() as $key => $day) {
  1187. $weekdays[$key] = t(substr($day, 0, $length), array(), array('context' => $context));
  1188. }
  1189. $none = array('' => '');
  1190. return !$required ? $none + $weekdays : $weekdays;
  1191. }
  1192. /**
  1193. * Reorders weekdays to match the first day of the week.
  1194. *
  1195. * @param array $weekdays
  1196. * An array of weekdays.
  1197. *
  1198. * @return array
  1199. * An array of weekdays reordered to match the first day of the week.
  1200. */
  1201. function date_week_days_ordered($weekdays) {
  1202. $first_day = variable_get('date_first_day', 0);
  1203. if ($first_day > 0) {
  1204. for ($i = 1; $i <= $first_day; $i++) {
  1205. $last = array_shift($weekdays);
  1206. array_push($weekdays, $last);
  1207. }
  1208. }
  1209. return $weekdays;
  1210. }
  1211. /**
  1212. * Constructs an array of years.
  1213. *
  1214. * @param int $start
  1215. * The start year in the array.
  1216. * @param int $end
  1217. * The end year in the array.
  1218. * @param bool $required
  1219. * (optional) If FALSE, the returned array will include a blank value.
  1220. * Defaults to FALSE.
  1221. *
  1222. * @return array
  1223. * An array of years in the selected range.
  1224. */
  1225. function date_years($start = 0, $end = 0, $required = FALSE) {
  1226. // Ensure $min and $max are valid values.
  1227. if (empty($start)) {
  1228. $start = intval(date('Y', REQUEST_TIME) - 3);
  1229. }
  1230. if (empty($end)) {
  1231. $end = intval(date('Y', REQUEST_TIME) + 3);
  1232. }
  1233. $none = array(0 => '');
  1234. return !$required ? $none + drupal_map_assoc(range($start, $end)) : drupal_map_assoc(range($start, $end));
  1235. }
  1236. /**
  1237. * Constructs an array of days in a month.
  1238. *
  1239. * @param bool $required
  1240. * (optional) If FALSE, the returned array will include a blank value.
  1241. * Defaults to FALSE.
  1242. * @param int $month
  1243. * (optional) The month in which to find the number of days.
  1244. * @param int $year
  1245. * (optional) The year in which to find the number of days.
  1246. *
  1247. * @return array
  1248. * An array of days for the selected month.
  1249. */
  1250. function date_days($required = FALSE, $month = NULL, $year = NULL) {
  1251. // If we have a month and year, find the right last day of the month.
  1252. if (!empty($month) && !empty($year)) {
  1253. $date = new DateObject($year . '-' . $month . '-01 00:00:00', 'UTC');
  1254. $max = $date->format('t');
  1255. }
  1256. // If there is no month and year given, default to 31.
  1257. if (empty($max)) {
  1258. $max = 31;
  1259. }
  1260. $none = array(0 => '');
  1261. return !$required ? $none + drupal_map_assoc(range(1, $max)) : drupal_map_assoc(range(1, $max));
  1262. }
  1263. /**
  1264. * Constructs an array of hours.
  1265. *
  1266. * @param string $format
  1267. * A date format string.
  1268. * @param bool $required
  1269. * (optional) If FALSE, the returned array will include a blank value.
  1270. * Defaults to FALSE.
  1271. *
  1272. * @return array
  1273. * An array of hours in the selected format.
  1274. */
  1275. function date_hours($format = 'H', $required = FALSE) {
  1276. $hours = array();
  1277. if ($format == 'h' || $format == 'g') {
  1278. $min = 1;
  1279. $max = 12;
  1280. }
  1281. else {
  1282. $min = 0;
  1283. $max = 23;
  1284. }
  1285. for ($i = $min; $i <= $max; $i++) {
  1286. $hours[$i] = $i < 10 && ($format == 'H' || $format == 'h') ? "0$i" : $i;
  1287. }
  1288. $none = array('' => '');
  1289. return !$required ? $none + $hours : $hours;
  1290. }
  1291. /**
  1292. * Constructs an array of minutes.
  1293. *
  1294. * @param string $format
  1295. * A date format string.
  1296. * @param bool $required
  1297. * (optional) If FALSE, the returned array will include a blank value.
  1298. * Defaults to FALSE.
  1299. *
  1300. * @return array
  1301. * An array of minutes in the selected format.
  1302. */
  1303. function date_minutes($format = 'i', $required = FALSE, $increment = 1) {
  1304. $minutes = array();
  1305. // Ensure $increment has a value so we don't loop endlessly.
  1306. if (empty($increment)) {
  1307. $increment = 1;
  1308. }
  1309. for ($i = 0; $i < 60; $i += $increment) {
  1310. $minutes[$i] = $i < 10 && $format == 'i' ? "0$i" : $i;
  1311. }
  1312. $none = array('' => '');
  1313. return !$required ? $none + $minutes : $minutes;
  1314. }
  1315. /**
  1316. * Constructs an array of seconds.
  1317. *
  1318. * @param string $format
  1319. * A date format string.
  1320. * @param bool $required
  1321. * (optional) If FALSE, the returned array will include a blank value.
  1322. * Defaults to FALSE.
  1323. *
  1324. * @return array
  1325. * An array of seconds in the selected format.
  1326. */
  1327. function date_seconds($format = 's', $required = FALSE, $increment = 1) {
  1328. $seconds = array();
  1329. // Ensure $increment has a value so we don't loop endlessly.
  1330. if (empty($increment)) {
  1331. $increment = 1;
  1332. }
  1333. for ($i = 0; $i < 60; $i += $increment) {
  1334. $seconds[$i] = $i < 10 && $format == 's' ? "0$i" : $i;
  1335. }
  1336. $none = array('' => '');
  1337. return !$required ? $none + $seconds : $seconds;
  1338. }
  1339. /**
  1340. * Constructs an array of AM and PM options.
  1341. *
  1342. * @param bool $required
  1343. * (optional) If FALSE, the returned array will include a blank value.
  1344. * Defaults to FALSE.
  1345. *
  1346. * @return array
  1347. * An array of AM and PM options.
  1348. */
  1349. function date_ampm($required = FALSE) {
  1350. $none = array('' => '');
  1351. $ampm = array(
  1352. 'am' => t('am', array(), array('context' => 'ampm')),
  1353. 'pm' => t('pm', array(), array('context' => 'ampm')),
  1354. );
  1355. return !$required ? $none + $ampm : $ampm;
  1356. }
  1357. /**
  1358. * Constructs an array of regex replacement strings for date format elements.
  1359. *
  1360. * @param bool $strict
  1361. * Whether or not to force 2 digits for elements that sometimes allow either
  1362. * 1 or 2 digits.
  1363. *
  1364. * @return array
  1365. * An array of date() format letters and their regex equivalents.
  1366. */
  1367. function date_format_patterns($strict = FALSE) {
  1368. return array(
  1369. 'd' => '\d{' . ($strict ? '2' : '1,2') . '}',
  1370. 'm' => '\d{' . ($strict ? '2' : '1,2') . '}',
  1371. 'h' => '\d{' . ($strict ? '2' : '1,2') . '}',
  1372. 'H' => '\d{' . ($strict ? '2' : '1,2') . '}',
  1373. 'i' => '\d{' . ($strict ? '2' : '1,2') . '}',
  1374. 's' => '\d{' . ($strict ? '2' : '1,2') . '}',
  1375. 'j' => '\d{1,2}',
  1376. 'N' => '\d',
  1377. 'S' => '\w{2}',
  1378. 'w' => '\d',
  1379. 'z' => '\d{1,3}',
  1380. 'W' => '\d{1,2}',
  1381. 'n' => '\d{1,2}',
  1382. 't' => '\d{2}',
  1383. 'L' => '\d',
  1384. 'o' => '\d{4}',
  1385. 'Y' => '-?\d{1,6}',
  1386. 'y' => '\d{2}',
  1387. 'B' => '\d{3}',
  1388. 'g' => '\d{1,2}',
  1389. 'G' => '\d{1,2}',
  1390. 'e' => '\w*',
  1391. 'I' => '\d',
  1392. 'T' => '\w*',
  1393. 'U' => '\d*',
  1394. 'z' => '[+-]?\d*',
  1395. 'O' => '[+-]?\d{4}',
  1396. // Using S instead of w and 3 as well as 4 to pick up non-ASCII chars like
  1397. // German umlaut. Per http://drupal.org/node/1101284, we may need as little
  1398. // as 2 and as many as 5 characters in some languages.
  1399. 'D' => '\S{2,5}',
  1400. 'l' => '\S*',
  1401. 'M' => '\S{2,5}',
  1402. 'F' => '\S*',
  1403. 'P' => '[+-]?\d{2}\:\d{2}',
  1404. 'O' => '[+-]\d{4}',
  1405. 'c' => '(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})([+-]?\d{2}\:\d{2})',
  1406. 'r' => '(\w{3}), (\d{2})\s(\w{3})\s(\d{2,4})\s(\d{2}):(\d{2}):(\d{2})([+-]?\d{4})?',
  1407. );
  1408. }
  1409. /**
  1410. * Constructs an array of granularity options and their labels.
  1411. *
  1412. * @return array
  1413. * An array of translated date parts, keyed by their machine name.
  1414. */
  1415. function date_granularity_names() {
  1416. return array(
  1417. 'year' => t('Year', array(), array('context' => 'datetime')),
  1418. 'month' => t('Month', array(), array('context' => 'datetime')),
  1419. 'day' => t('Day', array(), array('context' => 'datetime')),
  1420. 'hour' => t('Hour', array(), array('context' => 'datetime')),
  1421. 'minute' => t('Minute', array(), array('context' => 'datetime')),
  1422. 'second' => t('Second', array(), array('context' => 'datetime')),
  1423. );
  1424. }
  1425. /**
  1426. * Sorts a granularity array.
  1427. *
  1428. * @param array $granularity
  1429. * An array of date parts.
  1430. */
  1431. function date_granularity_sorted($granularity) {
  1432. return array_intersect(array(
  1433. 'year',
  1434. 'month',
  1435. 'day',
  1436. 'hour',
  1437. 'minute',
  1438. 'second',
  1439. ), $granularity);
  1440. }
  1441. /**
  1442. * Constructs an array of granularity based on a given precision.
  1443. *
  1444. * @param string $precision
  1445. * A granularity item.
  1446. *
  1447. * @return array
  1448. * A granularity array containing the given precision and all those above it.
  1449. * For example, passing in 'month' will return array('year', 'month').
  1450. */
  1451. function date_granularity_array_from_precision($precision) {
  1452. $granularity_array = array('year', 'month', 'day', 'hour', 'minute', 'second');
  1453. switch ($precision) {
  1454. case 'year':
  1455. return array_slice($granularity_array, -6, 1);
  1456. case 'month':
  1457. return array_slice($granularity_array, -6, 2);
  1458. case 'day':
  1459. return array_slice($granularity_array, -6, 3);
  1460. case 'hour':
  1461. return array_slice($granularity_array, -6, 4);
  1462. case 'minute':
  1463. return array_slice($granularity_array, -6, 5);
  1464. default:
  1465. return $granularity_array;
  1466. }
  1467. }
  1468. /**
  1469. * Give a granularity array, return the highest precision.
  1470. *
  1471. * @param array $granularity_array
  1472. * An array of date parts.
  1473. *
  1474. * @return string
  1475. * The most precise element in a granularity array.
  1476. */
  1477. function date_granularity_precision($granularity_array) {
  1478. $input = date_granularity_sorted($granularity_array);
  1479. return array_pop($input);
  1480. }
  1481. /**
  1482. * Constructs a valid DATETIME format string for the granularity of an item.
  1483. *
  1484. * @todo This function is no longer used as of
  1485. * http://drupalcode.org/project/date.git/commit/07efbb5.
  1486. */
  1487. function date_granularity_format($granularity) {
  1488. if (is_array($granularity)) {
  1489. $granularity = date_granularity_precision($granularity);
  1490. }
  1491. $format = 'Y-m-d H:i:s';
  1492. switch ($granularity) {
  1493. case 'year':
  1494. return substr($format, 0, 1);
  1495. case 'month':
  1496. return substr($format, 0, 3);
  1497. case 'day':
  1498. return substr($format, 0, 5);
  1499. case 'hour';
  1500. return substr($format, 0, 7);
  1501. case 'minute':
  1502. return substr($format, 0, 9);
  1503. default:
  1504. return $format;
  1505. }
  1506. }
  1507. /**
  1508. * Returns a translated array of timezone names.
  1509. *
  1510. * Cache the untranslated array, make the translated array a static variable.
  1511. *
  1512. * @param bool $required
  1513. * (optional) If FALSE, the returned array will include a blank value.
  1514. * Defaults to FALSE.
  1515. * @param bool $refresh
  1516. * (optional) Whether to refresh the list. Defaults to TRUE.
  1517. *
  1518. * @return array
  1519. * An array of timezone names.
  1520. */
  1521. function date_timezone_names($required = FALSE, $refresh = FALSE) {
  1522. static $zonenames;
  1523. if (empty($zonenames) || $refresh) {
  1524. $cached = cache_get('date_timezone_identifiers_list');
  1525. $zonenames = !empty($cached) ? $cached->data : array();
  1526. if ($refresh || empty($cached) || empty($zonenames)) {
  1527. $data = timezone_identifiers_list();
  1528. asort($data);
  1529. foreach ($data as $delta => $zone) {
  1530. // Because many timezones exist in PHP only for backward compatibility
  1531. // reasons and should not be used, the list is filtered by a regular
  1532. // expression.
  1533. if (preg_match('!^((Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)/|UTC$)!', $zone)) {
  1534. $zonenames[$zone] = $zone;
  1535. }
  1536. }
  1537. if (!empty($zonenames)) {
  1538. cache_set('date_timezone_identifiers_list', $zonenames);
  1539. }
  1540. }
  1541. foreach ($zonenames as $zone) {
  1542. $zonenames[$zone] = t('!timezone', array('!timezone' => t($zone)));
  1543. }
  1544. }
  1545. $none = array('' => '');
  1546. return !$required ? $none + $zonenames : $zonenames;
  1547. }
  1548. /**
  1549. * Returns an array of system-allowed timezone abbreviations.
  1550. *
  1551. * Cache an array of just the abbreviation names because the whole
  1552. * timezone_abbreviations_list() is huge, so we don't want to retrieve it more
  1553. * than necessary.
  1554. *
  1555. * @param bool $refresh
  1556. * (optional) Whether to refresh the list. Defaults to TRUE.
  1557. *
  1558. * @return array
  1559. * An array of allowed timezone abbreviations.
  1560. */
  1561. function date_timezone_abbr($refresh = FALSE) {
  1562. $cached = cache_get('date_timezone_abbreviations');
  1563. $data = isset($cached->data) ? $cached->data : array();
  1564. if (empty($data) || $refresh) {
  1565. $data = array_keys(timezone_abbreviations_list());
  1566. cache_set('date_timezone_abbreviations', $data);
  1567. }
  1568. return $data;
  1569. }
  1570. /**
  1571. * Formats a date, using a date type or a custom date format string.
  1572. *
  1573. * Reworked from Drupal's format_date function to handle pre-1970 and
  1574. * post-2038 dates and accept a date object instead of a timestamp as input.
  1575. * Translates formatted date results, unlike PHP function date_format().
  1576. * Should only be used for display, not input, because it can't be parsed.
  1577. *
  1578. * @param object $date
  1579. * A date object.
  1580. * @param string $type
  1581. * (optional) The date format to use. Can be 'small', 'medium' or 'large' for
  1582. * the preconfigured date formats. If 'custom' is specified, then $format is
  1583. * required as well. Defaults to 'medium'.
  1584. * @param string $format
  1585. * (optional) A PHP date format string as required by date(). A backslash
  1586. * should be used before a character to avoid interpreting the character as
  1587. * part of a date format. Defaults to an empty string.
  1588. * @param string $langcode
  1589. * (optional) Language code to translate to. Defaults to NULL.
  1590. *
  1591. * @return string
  1592. * A translated date string in the requested format.
  1593. *
  1594. * @see format_date()
  1595. */
  1596. function date_format_date($date, $type = 'medium', $format = '', $langcode = NULL) {
  1597. if (empty($date)) {
  1598. return '';
  1599. }
  1600. if ($type != 'custom') {
  1601. $format = variable_get('date_format_' . $type);
  1602. }
  1603. if ($type != 'custom' && empty($format)) {
  1604. $format = variable_get('date_format_medium', 'D, m/d/Y - H:i');
  1605. }
  1606. $format = date_limit_format($format, $date->granularity);
  1607. $max = strlen($format);
  1608. $datestring = '';
  1609. for ($i = 0; $i < $max; $i++) {
  1610. $c = $format[$i];
  1611. switch ($c) {
  1612. case 'l':
  1613. $datestring .= t($date->format('l'), array(), array('context' => '', 'langcode' => $langcode));
  1614. break;
  1615. case 'D':
  1616. $datestring .= t($date->format('D'), array(), array('context' => '', 'langcode' => $langcode));
  1617. break;
  1618. case 'F':
  1619. $datestring .= t($date->format('F'), array(), array('context' => 'Long month name', 'langcode' => $langcode));
  1620. break;
  1621. case 'M':
  1622. $datestring .= t($date->format('M'), array(), array('langcode' => $langcode));
  1623. break;
  1624. case 'A':
  1625. case 'a':
  1626. $datestring .= t($date->format($c), array(), array('context' => 'ampm', 'langcode' => $langcode));
  1627. break;
  1628. // The timezone name translations can use t().
  1629. case 'e':
  1630. case 'T':
  1631. $datestring .= t($date->format($c));
  1632. break;
  1633. // Remaining date parts need no translation.
  1634. case 'O':
  1635. $datestring .= sprintf('%s%02d%02d', (date_offset_get($date) < 0 ? '-' : '+'), abs(date_offset_get($date) / 3600), abs(date_offset_get($date) % 3600) / 60);
  1636. break;
  1637. case 'P':
  1638. $datestring .= sprintf('%s%02d:%02d', (date_offset_get($date) < 0 ? '-' : '+'), abs(date_offset_get($date) / 3600), abs(date_offset_get($date) % 3600) / 60);
  1639. break;
  1640. case 'Z':
  1641. $datestring .= date_offset_get($date);
  1642. break;
  1643. case '\\':
  1644. $datestring .= $format[++$i];
  1645. break;
  1646. case 'r':
  1647. $datestring .= date_format_date($date, 'custom', 'D, d M Y H:i:s O', 'en');
  1648. break;
  1649. default:
  1650. if (strpos('BdcgGhHiIjLmnNosStTuUwWYyz', $c) !== FALSE) {
  1651. $datestring .= $date->format($c);
  1652. }
  1653. else {
  1654. $datestring .= $c;
  1655. }
  1656. }
  1657. }
  1658. return $datestring;
  1659. }
  1660. /**
  1661. * Formats a time interval with granularity, including past and future context.
  1662. *
  1663. * @param object $date
  1664. * The current date object.
  1665. * @param int $granularity
  1666. * (optional) Number of units to display in the string. Defaults to 2.
  1667. *
  1668. * @return string
  1669. * A translated string representation of the interval.
  1670. *
  1671. * @see format_interval()
  1672. */
  1673. function date_format_interval($date, $granularity = 2, $display_ago = TRUE) {
  1674. // If no date is sent, then return nothing.
  1675. if (empty($date)) {
  1676. return NULL;
  1677. }
  1678. $interval = REQUEST_TIME - $date->format('U');
  1679. if ($interval > 0) {
  1680. return $display_ago ? t('!time ago', array('!time' => format_interval($interval, $granularity))) :
  1681. t('!time', array('!time' => format_interval($interval, $granularity)));
  1682. }
  1683. else {
  1684. return format_interval(abs($interval), $granularity);
  1685. }
  1686. }
  1687. /**
  1688. * A date object for the current time.
  1689. *
  1690. * @param object|string|null $timezone
  1691. * (optional) PHP DateTimeZone object, string or NULL allowed. Optionally
  1692. * force time to a specific timezone, defaults to user timezone, if set,
  1693. * otherwise site timezone. Defaults to NULL.
  1694. *
  1695. * @param bool $reset
  1696. * (optional) Static cache reset.
  1697. *
  1698. * @return object
  1699. * The current time as a date object.
  1700. */
  1701. function date_now($timezone = NULL, $reset = FALSE) {
  1702. $static_var = __FUNCTION__ . $timezone;
  1703. if ($timezone instanceof DateTimeZone) {
  1704. $static_var = __FUNCTION__ . $timezone->getName();
  1705. }
  1706. if ($reset) {
  1707. drupal_static_reset($static_var);
  1708. }
  1709. $now = &drupal_static($static_var);
  1710. if (!isset($now)) {
  1711. $now = new DateObject('now', $timezone);
  1712. }
  1713. // Avoid unexpected manipulation of cached $now object
  1714. // by subsequent code execution
  1715. // @see https://drupal.org/node/2261395
  1716. $clone = clone $now;
  1717. return $clone;
  1718. }
  1719. /**
  1720. * Determines if a timezone string is valid.
  1721. *
  1722. * @param string $timezone
  1723. * A potentially invalid timezone string.
  1724. *
  1725. * @return bool
  1726. * TRUE if the timezone is valid, FALSE otherwise.
  1727. */
  1728. function date_timezone_is_valid($timezone) {
  1729. static $timezone_names;
  1730. if (empty($timezone_names)) {
  1731. $timezone_names = array_keys(date_timezone_names(TRUE));
  1732. }
  1733. return in_array($timezone, $timezone_names);
  1734. }
  1735. /**
  1736. * Returns a timezone name to use as a default.
  1737. *
  1738. * @param bool $check_user
  1739. * (optional) Whether or not to check for a user-configured timezone.
  1740. * Defaults to TRUE.
  1741. *
  1742. * @return string
  1743. * The default timezone for a user, if available, otherwise the site.
  1744. */
  1745. function date_default_timezone($check_user = TRUE) {
  1746. global $user;
  1747. if ($check_user && variable_get('configurable_timezones', 1) && !empty($user->timezone)) {
  1748. return $user->timezone;
  1749. }
  1750. else {
  1751. $default = variable_get('date_default_timezone', '');
  1752. return empty($default) ? 'UTC' : $default;
  1753. }
  1754. }
  1755. /**
  1756. * Returns a timezone object for the default timezone.
  1757. *
  1758. * @param bool $check_user
  1759. * (optional) Whether or not to check for a user-configured timezone.
  1760. * Defaults to TRUE.
  1761. *
  1762. * @return object
  1763. * The default timezone for a user, if available, otherwise the site.
  1764. */
  1765. function date_default_timezone_object($check_user = TRUE) {
  1766. return timezone_open(date_default_timezone($check_user));
  1767. }
  1768. /**
  1769. * Identifies the number of days in a month for a date.
  1770. */
  1771. function date_days_in_month($year, $month) {
  1772. // Pick a day in the middle of the month to avoid timezone shifts.
  1773. $datetime = date_pad($year, 4) . '-' . date_pad($month) . '-15 00:00:00';
  1774. $date = new DateObject($datetime);
  1775. if ($date->errors) {
  1776. return FALSE;
  1777. }
  1778. else {
  1779. return $date->format('t');
  1780. }
  1781. }
  1782. /**
  1783. * Identifies the number of days in a year for a date.
  1784. *
  1785. * @param mixed $date
  1786. * (optional) The current date object, or a date string. Defaults to NULL.
  1787. *
  1788. * @return int
  1789. * The number of days in the year.
  1790. */
  1791. function date_days_in_year($date = NULL) {
  1792. if (empty($date)) {
  1793. $date = date_now();
  1794. }
  1795. elseif (!is_object($date)) {
  1796. $date = new DateObject($date);
  1797. }
  1798. if (is_object($date)) {
  1799. if ($date->format('L')) {
  1800. return 366;
  1801. }
  1802. else {
  1803. return 365;
  1804. }
  1805. }
  1806. return NULL;
  1807. }
  1808. /**
  1809. * Identifies the number of ISO weeks in a year for a date.
  1810. *
  1811. * December 28 is always in the last ISO week of the year.
  1812. *
  1813. * @param mixed $date
  1814. * (optional) The current date object, or a date string. Defaults to NULL.
  1815. *
  1816. * @return int
  1817. * The number of ISO weeks in a year.
  1818. */
  1819. function date_iso_weeks_in_year($date = NULL) {
  1820. if (empty($date)) {
  1821. $date = date_now();
  1822. }
  1823. elseif (!is_object($date)) {
  1824. $date = new DateObject($date);
  1825. }
  1826. if (is_object($date)) {
  1827. date_date_set($date, $date->format('Y'), 12, 28);
  1828. return $date->format('W');
  1829. }
  1830. return NULL;
  1831. }
  1832. /**
  1833. * Returns day of week for a given date (0 = Sunday).
  1834. *
  1835. * @param mixed $date
  1836. * (optional) A date, default is current local day. Defaults to NULL.
  1837. *
  1838. * @return int
  1839. * The number of the day in the week.
  1840. */
  1841. function date_day_of_week($date = NULL) {
  1842. if (empty($date)) {
  1843. $date = date_now();
  1844. }
  1845. elseif (!is_object($date)) {
  1846. $date = new DateObject($date);
  1847. }
  1848. if (is_object($date)) {
  1849. return $date->format('w');
  1850. }
  1851. return NULL;
  1852. }
  1853. /**
  1854. * Returns translated name of the day of week for a given date.
  1855. *
  1856. * @param mixed $date
  1857. * (optional) A date, default is current local day. Defaults to NULL.
  1858. * @param string $abbr
  1859. * (optional) Whether to return the abbreviated name for that day.
  1860. * Defaults to TRUE.
  1861. *
  1862. * @return string
  1863. * The name of the day in the week for that date.
  1864. */
  1865. function date_day_of_week_name($date = NULL, $abbr = TRUE) {
  1866. if (!is_object($date)) {
  1867. $date = new DateObject($date);
  1868. }
  1869. $dow = date_day_of_week($date);
  1870. $days = $abbr ? date_week_days_abbr() : date_week_days();
  1871. return $days[$dow];
  1872. }
  1873. /**
  1874. * Calculates the start and end dates for a calendar week.
  1875. *
  1876. * The dates are adjusted to use the chosen first day of week for this site.
  1877. *
  1878. * @param int $week
  1879. * The week value.
  1880. * @param int $year
  1881. * The year value.
  1882. *
  1883. * @return array
  1884. * A numeric array containing the start and end dates of a week.
  1885. */
  1886. function date_week_range($week, $year) {
  1887. if (variable_get('date_api_use_iso8601', FALSE)) {
  1888. return date_iso_week_range($week, $year);
  1889. }
  1890. $min_date = new DateObject($year . '-01-01 00:00:00');
  1891. $min_date->setTimezone(date_default_timezone_object());
  1892. // Move to the right week.
  1893. date_modify($min_date, '+' . strval(7 * ($week - 1)) . ' days');
  1894. // Move backwards to the first day of the week.
  1895. $first_day = variable_get('date_first_day', 0);
  1896. $day_wday = date_format($min_date, 'w');
  1897. date_modify($min_date, '-' . strval((7 + $day_wday - $first_day) % 7) . ' days');
  1898. // Move forwards to the last day of the week.
  1899. $max_date = clone($min_date);
  1900. date_modify($max_date, '+6 days');
  1901. if (date_format($min_date, 'Y') != $year) {
  1902. $min_date = new DateObject($year . '-01-01 00:00:00');
  1903. }
  1904. return array($min_date, $max_date);
  1905. }
  1906. /**
  1907. * Calculates the start and end dates for an ISO week.
  1908. *
  1909. * @param int $week
  1910. * The week value.
  1911. * @param int $year
  1912. * The year value.
  1913. *
  1914. * @return array
  1915. * A numeric array containing the start and end dates of an ISO week.
  1916. */
  1917. function date_iso_week_range($week, $year) {
  1918. // Get to the last ISO week of the previous year.
  1919. $min_date = new DateObject(($year - 1) . '-12-28 00:00:00');
  1920. date_timezone_set($min_date, date_default_timezone_object());
  1921. // Find the first day of the first ISO week in the year.
  1922. // If it's already a Monday, date_modify won't add a Monday,
  1923. // it will remain the same day. So add a Sunday first, then a Monday.
  1924. date_modify($min_date, '+1 Sunday');
  1925. date_modify($min_date, '+1 Monday');
  1926. // Jump ahead to the desired week for the beginning of the week range.
  1927. if ($week > 1) {
  1928. date_modify($min_date, '+ ' . ($week - 1) . ' weeks');
  1929. }
  1930. // Move forwards to the last day of the week.
  1931. $max_date = clone($min_date);
  1932. date_modify($max_date, '+6 days');
  1933. return array($min_date, $max_date);
  1934. }
  1935. /**
  1936. * The number of calendar weeks in a year.
  1937. *
  1938. * PHP week functions return the ISO week, not the calendar week.
  1939. *
  1940. * @param int $year
  1941. * A year value.
  1942. *
  1943. * @return int
  1944. * Number of calendar weeks in selected year.
  1945. */
  1946. function date_weeks_in_year($year) {
  1947. $date = new DateObject(($year + 1) . '-01-01 12:00:00', 'UTC');
  1948. date_modify($date, '-1 day');
  1949. return date_week($date->format('Y-m-d'));
  1950. }
  1951. /**
  1952. * The calendar week number for a date.
  1953. *
  1954. * PHP week functions return the ISO week, not the calendar week.
  1955. *
  1956. * @param string $date
  1957. * A date string in the format Y-m-d.
  1958. *
  1959. * @return int
  1960. * The calendar week number.
  1961. */
  1962. function date_week($date) {
  1963. $date = substr($date, 0, 10);
  1964. $parts = explode('-', $date);
  1965. $date = new DateObject($date . ' 12:00:00', 'UTC');
  1966. // If we are using ISO weeks, this is easy.
  1967. if (variable_get('date_api_use_iso8601', FALSE)) {
  1968. return intval($date->format('W'));
  1969. }
  1970. $year_date = new DateObject($parts[0] . '-01-01 12:00:00', 'UTC');
  1971. $week = intval($date->format('W'));
  1972. $year_week = intval(date_format($year_date, 'W'));
  1973. $date_year = intval($date->format('o'));
  1974. // Remove the leap week if it's present.
  1975. if ($date_year > intval($parts[0])) {
  1976. $last_date = clone($date);
  1977. date_modify($last_date, '-7 days');
  1978. $week = date_format($last_date, 'W') + 1;
  1979. }
  1980. elseif ($date_year < intval($parts[0])) {
  1981. $week = 0;
  1982. }
  1983. if ($year_week != 1) {
  1984. $week++;
  1985. }
  1986. // Convert to ISO-8601 day number, to match weeks calculated above.
  1987. $iso_first_day = 1 + (variable_get('date_first_day', 0) + 6) % 7;
  1988. // If it's before the starting day, it's the previous week.
  1989. if (intval($date->format('N')) < $iso_first_day) {
  1990. $week--;
  1991. }
  1992. // If the year starts before, it's an extra week at the beginning.
  1993. if (intval(date_format($year_date, 'N')) < $iso_first_day) {
  1994. $week++;
  1995. }
  1996. return $week;
  1997. }
  1998. /**
  1999. * Helper function to left pad date parts with zeros.
  2000. *
  2001. * Provided because this is needed so often with dates.
  2002. *
  2003. * @param int $value
  2004. * The value to pad.
  2005. * @param int $size
  2006. * (optional) Total size expected, usually 2 or 4. Defaults to 2.
  2007. *
  2008. * @return string
  2009. * The padded value.
  2010. */
  2011. function date_pad($value, $size = 2) {
  2012. return sprintf("%0" . $size . "d", $value);
  2013. }
  2014. /**
  2015. * Determines if the granularity contains a time portion.
  2016. *
  2017. * @param array $granularity
  2018. * An array of allowed date parts, all others will be removed.
  2019. *
  2020. * @return bool
  2021. * TRUE if the granularity contains a time portion, FALSE otherwise.
  2022. */
  2023. function date_has_time($granularity) {
  2024. if (!is_array($granularity)) {
  2025. $granularity = array();
  2026. }
  2027. $options = array('hour', 'minute', 'second');
  2028. return (bool) count(array_intersect($granularity, $options));
  2029. }
  2030. /**
  2031. * Determines if the granularity contains a date portion.
  2032. *
  2033. * @param array $granularity
  2034. * An array of allowed date parts, all others will be removed.
  2035. *
  2036. * @return bool
  2037. * TRUE if the granularity contains a date portion, FALSE otherwise.
  2038. */
  2039. function date_has_date($granularity) {
  2040. if (!is_array($granularity)) {
  2041. $granularity = array();
  2042. }
  2043. $options = array('year', 'month', 'day');
  2044. return (bool) count(array_intersect($granularity, $options));
  2045. }
  2046. /**
  2047. * Helper function to get a format for a specific part of a date field.
  2048. *
  2049. * @param string $part
  2050. * The date field part, either 'time' or 'date'.
  2051. * @param string $format
  2052. * A date format string.
  2053. *
  2054. * @return string
  2055. * The date format for the given part.
  2056. */
  2057. function date_part_format($part, $format) {
  2058. switch ($part) {
  2059. case 'date':
  2060. return date_limit_format($format, array('year', 'month', 'day'));
  2061. case 'time':
  2062. return date_limit_format($format, array('hour', 'minute', 'second'));
  2063. default:
  2064. return date_limit_format($format, array($part));
  2065. }
  2066. }
  2067. /**
  2068. * Limits a date format to include only elements from a given granularity array.
  2069. *
  2070. * Example:
  2071. * date_limit_format('F j, Y - H:i', array('year', 'month', 'day'));
  2072. * returns 'F j, Y'
  2073. *
  2074. * @param string $format
  2075. * A date format string.
  2076. * @param array $granularity
  2077. * An array of allowed date parts, all others will be removed.
  2078. *
  2079. * @return string
  2080. * The format string with all other elements removed.
  2081. */
  2082. function date_limit_format($format, $granularity) {
  2083. // Use the advanced drupal_static() pattern to improve performance.
  2084. static $drupal_static_fast;
  2085. if (!isset($drupal_static_fast)) {
  2086. $drupal_static_fast['formats'] = &drupal_static(__FUNCTION__);
  2087. }
  2088. $formats = &$drupal_static_fast['formats'];
  2089. $format_granularity_cid = $format . '|' . implode(',', $granularity);
  2090. if (isset($formats[$format_granularity_cid])) {
  2091. return $formats[$format_granularity_cid];
  2092. }
  2093. // If punctuation has been escaped, remove the escaping. Done using strtr()
  2094. // because it is easier than getting the escape character extracted using
  2095. // preg_replace().
  2096. $replace = array(
  2097. '\-' => '-',
  2098. '\:' => ':',
  2099. "\'" => "'",
  2100. '\. ' => ' . ',
  2101. '\,' => ',',
  2102. );
  2103. $format = strtr($format, $replace);
  2104. // Get the 'T' out of ISO date formats that don't have both date and time.
  2105. if (!date_has_time($granularity) || !date_has_date($granularity)) {
  2106. $format = str_replace('\T', ' ', $format);
  2107. $format = str_replace('T', ' ', $format);
  2108. }
  2109. $regex = array();
  2110. if (!date_has_time($granularity)) {
  2111. $regex[] = '((?<!\\\\)[a|A])';
  2112. }
  2113. // Create regular expressions to remove selected values from string.
  2114. // Use (?<!\\\\) to keep escaped letters from being removed.
  2115. foreach (date_nongranularity($granularity) as $element) {
  2116. switch ($element) {
  2117. case 'year':
  2118. $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[Yy])';
  2119. break;
  2120. case 'day':
  2121. $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[l|D|d|dS|j|jS|N|w|W|z]{1,2})';
  2122. break;
  2123. case 'month':
  2124. $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[FMmn])';
  2125. break;
  2126. case 'hour':
  2127. $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[HhGg])';
  2128. break;
  2129. case 'minute':
  2130. $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[i])';
  2131. break;
  2132. case 'second':
  2133. $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[s])';
  2134. break;
  2135. case 'timezone':
  2136. $regex[] = '([\-/\.,:]?\s?(?<!\\\\)[TOZPe])';
  2137. break;
  2138. }
  2139. }
  2140. // Remove empty parentheses, brackets, pipes.
  2141. $regex[] = '(\(\))';
  2142. $regex[] = '(\[\])';
  2143. $regex[] = '(\|\|)';
  2144. // Remove selected values from string.
  2145. $format = trim(preg_replace($regex, array(), $format));
  2146. // Remove orphaned punctuation at the beginning of the string.
  2147. $format = preg_replace('`^([\-/\.,:\'])`', '', $format);
  2148. // Remove orphaned punctuation at the end of the string.
  2149. $format = preg_replace('([\-/,:\']$)', '', $format);
  2150. $format = preg_replace('(\\$)', '', $format);
  2151. // Trim any whitespace from the result.
  2152. $format = trim($format);
  2153. // After removing the non-desired parts of the format, test if the only things
  2154. // left are escaped, non-date, characters. If so, return nothing.
  2155. // Using S instead of w to pick up non-ASCII characters.
  2156. $test = trim(preg_replace('(\\\\\S{1,3})u', '', $format));
  2157. if (empty($test)) {
  2158. $format = '';
  2159. }
  2160. // Store the return value in the static array for performance.
  2161. $formats[$format_granularity_cid] = $format;
  2162. return $format;
  2163. }
  2164. /**
  2165. * Converts a format to an ordered array of granularity parts.
  2166. *
  2167. * Example:
  2168. * date_format_order('m/d/Y H:i')
  2169. * returns
  2170. * array(
  2171. * 0 => 'month',
  2172. * 1 => 'day',
  2173. * 2 => 'year',
  2174. * 3 => 'hour',
  2175. * 4 => 'minute',
  2176. * );
  2177. *
  2178. * @param string $format
  2179. * A date format string.
  2180. *
  2181. * @return array
  2182. * An array of ordered granularity elements from the given format string.
  2183. */
  2184. function date_format_order($format) {
  2185. $order = array();
  2186. if (empty($format)) {
  2187. return $order;
  2188. }
  2189. $max = strlen($format);
  2190. for ($i = 0; $i <= $max; $i++) {
  2191. if (!isset($format[$i])) {
  2192. break;
  2193. }
  2194. switch ($format[$i]) {
  2195. case 'd':
  2196. case 'j':
  2197. $order[] = 'day';
  2198. break;
  2199. case 'F':
  2200. case 'M':
  2201. case 'm':
  2202. case 'n':
  2203. $order[] = 'month';
  2204. break;
  2205. case 'Y':
  2206. case 'y':
  2207. $order[] = 'year';
  2208. break;
  2209. case 'g':
  2210. case 'G':
  2211. case 'h':
  2212. case 'H':
  2213. $order[] = 'hour';
  2214. break;
  2215. case 'i':
  2216. $order[] = 'minute';
  2217. break;
  2218. case 's':
  2219. $order[] = 'second';
  2220. break;
  2221. }
  2222. }
  2223. return $order;
  2224. }
  2225. /**
  2226. * Strips out unwanted granularity elements.
  2227. *
  2228. * @param array $granularity
  2229. * An array like ('year', 'month', 'day', 'hour', 'minute', 'second');
  2230. *
  2231. * @return array
  2232. * A reduced set of granularitiy elements.
  2233. */
  2234. function date_nongranularity($granularity) {
  2235. $options = array(
  2236. 'year',
  2237. 'month',
  2238. 'day',
  2239. 'hour',
  2240. 'minute',
  2241. 'second',
  2242. 'timezone',
  2243. );
  2244. return array_diff($options, (array) $granularity);
  2245. }
  2246. /**
  2247. * Implements hook_element_info().
  2248. */
  2249. function date_api_element_info() {
  2250. module_load_include('inc', 'date_api', 'date_api_elements');
  2251. return _date_api_element_info();
  2252. }
  2253. /**
  2254. * Implements hook_theme().
  2255. */
  2256. function date_api_theme($existing, $type, $theme, $path) {
  2257. $base = array(
  2258. 'file' => 'theme.inc',
  2259. 'path' => "$path/theme",
  2260. );
  2261. return array(
  2262. 'date_nav_title' => $base + array(
  2263. 'variables' => array(
  2264. 'granularity' => NULL, 'view' => NULL, 'link' => NULL, 'format' => NULL
  2265. ),
  2266. ),
  2267. 'date_timezone' => $base + array('render element' => 'element'),
  2268. 'date_select' => $base + array('render element' => 'element'),
  2269. 'date_text' => $base + array('render element' => 'element'),
  2270. 'date_select_element' => $base + array('render element' => 'element'),
  2271. 'date_textfield_element' => $base + array('render element' => 'element'),
  2272. 'date_part_hour_prefix' => $base + array('render element' => 'element'),
  2273. 'date_part_minsec_prefix' => $base + array('render element' => 'element'),
  2274. 'date_part_label_year' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
  2275. 'date_part_label_month' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
  2276. 'date_part_label_day' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
  2277. 'date_part_label_hour' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
  2278. 'date_part_label_minute' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
  2279. 'date_part_label_second' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
  2280. 'date_part_label_ampm' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
  2281. 'date_part_label_timezone' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
  2282. 'date_part_label_date' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
  2283. 'date_part_label_time' => $base + array('variables' => array('date_part' => NULL, 'element' => NULL)),
  2284. 'date_views_filter_form' => $base + array('template' => 'date-views-filter-form', 'render element' => 'form'),
  2285. 'date_calendar_day' => $base + array('variables' => array('date' => NULL)),
  2286. 'date_time_ago' => $base + array(
  2287. 'variables' => array(
  2288. 'start_date' => NULL, 'end_date' => NULL, 'interval' => NULL
  2289. ),
  2290. ),
  2291. );
  2292. }
  2293. /**
  2294. * Function to figure out which local timezone applies to a date and select it.
  2295. *
  2296. * @param string $handling
  2297. * The timezone handling.
  2298. * @param string $timezone
  2299. * (optional) A timezone string. Defaults to an empty string.
  2300. *
  2301. * @return string
  2302. * The timezone string.
  2303. */
  2304. function date_get_timezone($handling, $timezone = '') {
  2305. switch ($handling) {
  2306. case 'date':
  2307. $timezone = !empty($timezone) ? $timezone : date_default_timezone();
  2308. break;
  2309. case 'utc':
  2310. $timezone = 'UTC';
  2311. break;
  2312. default:
  2313. $timezone = date_default_timezone();
  2314. }
  2315. return $timezone > '' ? $timezone : date_default_timezone();
  2316. }
  2317. /**
  2318. * Function to figure out which db timezone applies to a date.
  2319. *
  2320. * @param string $handling
  2321. * The timezone handling.
  2322. * @param string $timezone
  2323. * (optional) When $handling is 'date', date_get_timezone_db() returns this
  2324. * value.
  2325. *
  2326. * @return string
  2327. * The timezone string.
  2328. */
  2329. function date_get_timezone_db($handling, $timezone = NULL) {
  2330. switch ($handling) {
  2331. case ('utc'):
  2332. case ('site'):
  2333. case ('user'):
  2334. // These handling modes all convert to UTC before storing in the DB.
  2335. $timezone = 'UTC';
  2336. break;
  2337. case ('date'):
  2338. if ($timezone == NULL) {
  2339. // This shouldn't happen, since it's meaning is undefined. But we need
  2340. // to fall back to *something* that's a legal timezone.
  2341. $timezone = date_default_timezone();
  2342. }
  2343. break;
  2344. case ('none'):
  2345. default:
  2346. $timezone = date_default_timezone();
  2347. break;
  2348. }
  2349. return $timezone;
  2350. }
  2351. /**
  2352. * Helper function for converting back and forth from '+1' to 'First'.
  2353. */
  2354. function date_order_translated() {
  2355. return array(
  2356. '+1' => t('First', array(), array('context' => 'date_order')),
  2357. '+2' => t('Second', array(), array('context' => 'date_order')),
  2358. '+3' => t('Third', array(), array('context' => 'date_order')),
  2359. '+4' => t('Fourth', array(), array('context' => 'date_order')),
  2360. '+5' => t('Fifth', array(), array('context' => 'date_order')),
  2361. '-1' => t('Last', array(), array('context' => 'date_order_reverse')),
  2362. '-2' => t('Next to last', array(), array('context' => 'date_order_reverse')),
  2363. '-3' => t('Third from last', array(), array('context' => 'date_order_reverse')),
  2364. '-4' => t('Fourth from last', array(), array('context' => 'date_order_reverse')),
  2365. '-5' => t('Fifth from last', array(), array('context' => 'date_order_reverse')),
  2366. );
  2367. }
  2368. /**
  2369. * Creates an array of ordered strings, using English text when possible.
  2370. */
  2371. function date_order() {
  2372. return array(
  2373. '+1' => 'First',
  2374. '+2' => 'Second',
  2375. '+3' => 'Third',
  2376. '+4' => 'Fourth',
  2377. '+5' => 'Fifth',
  2378. '-1' => 'Last',
  2379. '-2' => '-2',
  2380. '-3' => '-3',
  2381. '-4' => '-4',
  2382. '-5' => '-5',
  2383. );
  2384. }
  2385. /**
  2386. * Tests validity of a date range string.
  2387. *
  2388. * @param string $string
  2389. * A min and max year string like '-3:+1'a.
  2390. *
  2391. * @return bool
  2392. * TRUE if the date range is valid, FALSE otherwise.
  2393. */
  2394. function date_range_valid($string) {
  2395. $matches = preg_match('@^([\+\-][0-9]+|[0-9]{4}):([\+\-][0-9]+|[0-9]{4})$@', $string);
  2396. return $matches < 1 ? FALSE : TRUE;
  2397. }
  2398. /**
  2399. * Splits a string like -3:+3 or 2001:2010 into an array of start and end years.
  2400. *
  2401. * Center the range around the current year, if any, but expand it far
  2402. * enough so it will pick up the year value in the field in case
  2403. * the value in the field is outside the initial range.
  2404. *
  2405. * @param string $string
  2406. * A min and max year string like '-3:+1'.
  2407. * @param object $date
  2408. * (optional) A date object. Defaults to NULL.
  2409. *
  2410. * @return array
  2411. * A numerically indexed array, containing a start and end year.
  2412. */
  2413. function date_range_years($string, $date = NULL) {
  2414. $this_year = date_format(date_now(), 'Y');
  2415. list($start_year, $end_year) = explode(':', $string);
  2416. // Valid patterns would be -5:+5, 0:+1, 2008:2010.
  2417. $plus_pattern = '@[\+\-][0-9]{1,4}@';
  2418. $year_pattern = '@^[0-9]{4}@';
  2419. if (!preg_match($year_pattern, $start_year, $matches)) {
  2420. if (preg_match($plus_pattern, $start_year, $matches)) {
  2421. $start_year = $this_year + $matches[0];
  2422. }
  2423. else {
  2424. $start_year = $this_year;
  2425. }
  2426. }
  2427. if (!preg_match($year_pattern, $end_year, $matches)) {
  2428. if (preg_match($plus_pattern, $end_year, $matches)) {
  2429. $end_year = $this_year + $matches[0];
  2430. }
  2431. else {
  2432. $end_year = $this_year;
  2433. }
  2434. }
  2435. // If there is a current value, stretch the range to include it.
  2436. $value_year = is_object($date) ? $date->format('Y') : '';
  2437. if (!empty($value_year)) {
  2438. if ($start_year <= $end_year) {
  2439. $start_year = min($value_year, $start_year);
  2440. $end_year = max($value_year, $end_year);
  2441. }
  2442. else {
  2443. $start_year = max($value_year, $start_year);
  2444. $end_year = min($value_year, $end_year);
  2445. }
  2446. }
  2447. return array($start_year, $end_year);
  2448. }
  2449. /**
  2450. * Converts a min and max year into a string like '-3:+1'.
  2451. *
  2452. * @param array $years
  2453. * A numerically indexed array, containing a minimum and maximum year.
  2454. *
  2455. * @return string
  2456. * A min and max year string like '-3:+1'.
  2457. */
  2458. function date_range_string($years) {
  2459. $this_year = date_format(date_now(), 'Y');
  2460. if ($years[0] < $this_year) {
  2461. $min = '-' . ($this_year - $years[0]);
  2462. }
  2463. else {
  2464. $min = '+' . ($years[0] - $this_year);
  2465. }
  2466. if ($years[1] < $this_year) {
  2467. $max = '-' . ($this_year - $years[1]);
  2468. }
  2469. else {
  2470. $max = '+' . ($years[1] - $this_year);
  2471. }
  2472. return $min . ':' . $max;
  2473. }
  2474. /**
  2475. * Temporary helper to re-create equivalent of content_database_info().
  2476. */
  2477. function date_api_database_info($field, $revision = FIELD_LOAD_CURRENT) {
  2478. return array(
  2479. 'columns' => $field['storage']['details']['sql'][$revision],
  2480. 'table' => _field_sql_storage_tablename($field),
  2481. );
  2482. }
  2483. /**
  2484. * Implements hook_form_FORM_ID_alter() for system_regional_settings().
  2485. *
  2486. * Add a form element to configure whether or not week numbers are ISO-8601, the
  2487. * default is FALSE (US/UK/AUS norm).
  2488. */
  2489. function date_api_form_system_regional_settings_alter(&$form, &$form_state, $form_id) {
  2490. $form['locale']['date_api_use_iso8601'] = array(
  2491. '#type' => 'checkbox',
  2492. '#title' => t('Use ISO-8601 week numbers'),
  2493. '#default_value' => variable_get('date_api_use_iso8601', FALSE),
  2494. '#description' => t('IMPORTANT! If checked, First day of week MUST be set to Monday'),
  2495. );
  2496. $form['#validate'][] = 'date_api_form_system_settings_validate';
  2497. }
  2498. /**
  2499. * Validate that the option to use ISO weeks matches first day of week choice.
  2500. */
  2501. function date_api_form_system_settings_validate(&$form, &$form_state) {
  2502. $form_values = $form_state['values'];
  2503. if ($form_values['date_api_use_iso8601'] && $form_values['date_first_day'] != 1) {
  2504. form_set_error('date_first_day', t('When using ISO-8601 week numbers, the first day of the week must be set to Monday.'));
  2505. }
  2506. }
  2507. /**
  2508. * Creates an array of date format types for use as an options list.
  2509. */
  2510. function date_format_type_options() {
  2511. $options = array();
  2512. $format_types = system_get_date_types();
  2513. if (!empty($format_types)) {
  2514. foreach ($format_types as $type => $type_info) {
  2515. $options[$type] = $type_info['title'] . ' (' . date_format_date(date_example_date(), $type) . ')';
  2516. }
  2517. }
  2518. return $options;
  2519. }
  2520. /**
  2521. * Creates an example date.
  2522. *
  2523. * This ensures a clear difference between month and day, and 12 and 24 hours.
  2524. */
  2525. function date_example_date() {
  2526. $now = date_now();
  2527. if (date_format($now, 'M') == date_format($now, 'F')) {
  2528. date_modify($now, '+1 month');
  2529. }
  2530. if (date_format($now, 'm') == date_format($now, 'd')) {
  2531. date_modify($now, '+1 day');
  2532. }
  2533. if (date_format($now, 'H') == date_format($now, 'h')) {
  2534. date_modify($now, '+12 hours');
  2535. }
  2536. return $now;
  2537. }
  2538. /**
  2539. * Determine if a start/end date combination qualify as 'All day'.
  2540. *
  2541. * @param string $string1
  2542. * A string date in datetime format for the 'start' date.
  2543. * @param string $string2
  2544. * A string date in datetime format for the 'end' date.
  2545. * @param string $granularity
  2546. * (optional) The granularity of the date. Defaults to 'second'.
  2547. * @param int $increment
  2548. * (optional) The increment of the date. Defaults to 1.
  2549. *
  2550. * @return bool
  2551. * TRUE if the date is all day, FALSE otherwise.
  2552. */
  2553. function date_is_all_day($string1, $string2, $granularity = 'second', $increment = 1) {
  2554. if (empty($string1) || empty($string2)) {
  2555. return FALSE;
  2556. }
  2557. elseif (!in_array($granularity, array('hour', 'minute', 'second'))) {
  2558. return FALSE;
  2559. }
  2560. preg_match('/([0-9]{4}-[0-9]{2}-[0-9]{2}) (([0-9]{2}):([0-9]{2}):([0-9]{2}))/', $string1, $matches);
  2561. $count = count($matches);
  2562. $date1 = $count > 1 ? $matches[1] : '';
  2563. $time1 = $count > 2 ? $matches[2] : '';
  2564. $hour1 = $count > 3 ? intval($matches[3]) : 0;
  2565. $min1 = $count > 4 ? intval($matches[4]) : 0;
  2566. $sec1 = $count > 5 ? intval($matches[5]) : 0;
  2567. preg_match('/([0-9]{4}-[0-9]{2}-[0-9]{2}) (([0-9]{2}):([0-9]{2}):([0-9]{2}))/', $string2, $matches);
  2568. $count = count($matches);
  2569. $date2 = $count > 1 ? $matches[1] : '';
  2570. $time2 = $count > 2 ? $matches[2] : '';
  2571. $hour2 = $count > 3 ? intval($matches[3]) : 0;
  2572. $min2 = $count > 4 ? intval($matches[4]) : 0;
  2573. $sec2 = $count > 5 ? intval($matches[5]) : 0;
  2574. if (empty($date1) || empty($date2)) {
  2575. return FALSE;
  2576. }
  2577. if (empty($time1) || empty($time2)) {
  2578. return FALSE;
  2579. }
  2580. $tmp = date_seconds('s', TRUE, $increment);
  2581. $max_seconds = intval(array_pop($tmp));
  2582. $tmp = date_minutes('i', TRUE, $increment);
  2583. $max_minutes = intval(array_pop($tmp));
  2584. // See if minutes and seconds are the maximum allowed for an increment or the
  2585. // maximum possible (59), or 0.
  2586. switch ($granularity) {
  2587. case 'second':
  2588. $min_match = $time1 == '00:00:00'
  2589. || ($hour1 == 0 && $min1 == 0 && $sec1 == 0);
  2590. $max_match = $time2 == '00:00:00'
  2591. || ($hour2 == 23 && in_array($min2, array($max_minutes, 59)) && in_array($sec2, array($max_seconds, 59)))
  2592. || ($hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0 && $sec1 == 0 && $sec2 == 0);
  2593. break;
  2594. case 'minute':
  2595. $min_match = $time1 == '00:00:00'
  2596. || ($hour1 == 0 && $min1 == 0);
  2597. $max_match = $time2 == '00:00:00'
  2598. || ($hour2 == 23 && in_array($min2, array($max_minutes, 59)))
  2599. || ($hour1 == 0 && $hour2 == 0 && $min1 == 0 && $min2 == 0);
  2600. break;
  2601. case 'hour':
  2602. $min_match = $time1 == '00:00:00'
  2603. || ($hour1 == 0);
  2604. $max_match = $time2 == '00:00:00'
  2605. || ($hour2 == 23)
  2606. || ($hour1 == 0 && $hour2 == 0);
  2607. break;
  2608. default:
  2609. $min_match = TRUE;
  2610. $max_match = FALSE;
  2611. }
  2612. if ($min_match && $max_match) {
  2613. return TRUE;
  2614. }
  2615. return FALSE;
  2616. }
  2617. /**
  2618. * Helper function to round minutes and seconds to requested value.
  2619. */
  2620. function date_increment_round(&$date, $increment) {
  2621. // Round minutes and seconds, if necessary.
  2622. if (is_object($date) && $increment > 1) {
  2623. $day = intval(date_format($date, 'j'));
  2624. $hour = intval(date_format($date, 'H'));
  2625. $second = intval(round(intval(date_format($date, 's')) / $increment) * $increment);
  2626. $minute = intval(date_format($date, 'i'));
  2627. if ($second == 60) {
  2628. $minute += 1;
  2629. $second = 0;
  2630. }
  2631. $minute = intval(round($minute / $increment) * $increment);
  2632. if ($minute == 60) {
  2633. $hour += 1;
  2634. $minute = 0;
  2635. }
  2636. date_time_set($date, $hour, $minute, $second);
  2637. if ($hour == 24) {
  2638. $day += 1;
  2639. $hour = 0;
  2640. $year = date_format($date, 'Y');
  2641. $month = date_format($date, 'n');
  2642. date_date_set($date, $year, $month, $day);
  2643. }
  2644. }
  2645. return $date;
  2646. }
  2647. /**
  2648. * Determines if a date object is valid.
  2649. *
  2650. * @param object $date
  2651. * The date object to check.
  2652. *
  2653. * @return bool
  2654. * TRUE if the date is a valid date object, FALSE otherwise.
  2655. */
  2656. function date_is_date($date) {
  2657. if (empty($date) || !is_object($date) || !empty($date->errors)) {
  2658. return FALSE;
  2659. }
  2660. return TRUE;
  2661. }
  2662. /**
  2663. * Replace specific ISO values using patterns.
  2664. *
  2665. * Function will replace ISO values that have the pattern 9999-00-00T00:00:00
  2666. * with a pattern like 9999-01-01T00:00:00, to match the behavior of non-ISO dates
  2667. * and ensure that date objects created from this value contain a valid month
  2668. * and day.
  2669. * Without this fix, the ISO date '2020-00-00T00:00:00' would be created as
  2670. * November 30, 2019 (the previous day in the previous month).
  2671. *
  2672. * @param string $iso_string
  2673. * An ISO string that needs to be made into a complete, valid date.
  2674. *
  2675. * @return mixed|string
  2676. * replaced value, or incoming value.
  2677. *
  2678. * @TODO Expand on this to work with all sorts of partial ISO dates.
  2679. */
  2680. function date_make_iso_valid($iso_string) {
  2681. // If this isn't a value that uses an ISO pattern, there is nothing to do.
  2682. if (is_numeric($iso_string) || !preg_match(DATE_REGEX_ISO, $iso_string)) {
  2683. return $iso_string;
  2684. }
  2685. // First see if month and day parts are '-00-00'.
  2686. if (substr($iso_string, 4, 6) == '-00-00') {
  2687. return preg_replace('/([\d]{4}-)(00-00)(T[\d]{2}:[\d]{2}:[\d]{2})/', '${1}01-01${3}', $iso_string);
  2688. }
  2689. // Then see if the day part is '-00'.
  2690. elseif (substr($iso_string, 7, 3) == '-00') {
  2691. return preg_replace('/([\d]{4}-[\d]{2}-)(00)(T[\d]{2}:[\d]{2}:[\d]{2})/', '${1}01${3}', $iso_string);
  2692. }
  2693. // Fall through, no changes required.
  2694. return $iso_string;
  2695. }