date_api.module 84 KB

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