date_api_sql.inc 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144
  1. <?php
  2. /**
  3. * @file
  4. * SQL helper for Date API.
  5. *
  6. * @TODO
  7. * Add experimental support for sqlite: http://www.sqlite.org/lang_datefunc.html
  8. * and Oracle (http://psoug.org/reference/date_func.html and
  9. * http://psoug.org/reference/datatypes.html) date/time functions.
  10. */
  11. /**
  12. * A helper function to do cross-database concatation of date parts.
  13. *
  14. * @param array $array
  15. * An array of values to be concatonated in sql.
  16. *
  17. * @return string
  18. * Correct sql string for database type.
  19. */
  20. function date_sql_concat($array) {
  21. switch (Database::getConnection()->databaseType()) {
  22. case 'mysql':
  23. return "CONCAT(" . implode(",", $array) . ")";
  24. case 'pgsql':
  25. return implode(" || ", $array);
  26. }
  27. }
  28. /**
  29. * Helper function to do cross-database NULL replacements
  30. *
  31. * @param array $array
  32. * An array of values to test for NULL values.
  33. *
  34. * @return string
  35. * SQL statement to return the first non-NULL value in the list.
  36. */
  37. function date_sql_coalesce($array) {
  38. switch (Database::getConnection()->databaseType()) {
  39. case 'mysql':
  40. case 'pgsql':
  41. return "COALESCE(" . implode(',', $array) . ")";
  42. }
  43. }
  44. /**
  45. * A helper function to do cross-database padding of date parts.
  46. *
  47. * @param string $str
  48. * A string to apply padding to
  49. * @param int $size
  50. * The size the final string should be
  51. * @param string $pad
  52. * The value to pad the string with
  53. * @param string $side
  54. * The side of the string to pad
  55. */
  56. function date_sql_pad($str, $size = 2, $pad = '0', $side = 'l') {
  57. switch ($side) {
  58. case 'r':
  59. return "RPAD($str, $size, '$pad')";
  60. default:
  61. return "LPAD($str, $size, '$pad')";
  62. }
  63. }
  64. /**
  65. * A class to manipulate date SQL.
  66. */
  67. class date_sql_handler {
  68. var $db_type = NULL;
  69. var $date_type = DATE_DATETIME;
  70. // A string timezone name.
  71. var $db_timezone = 'UTC';
  72. // A string timezone name.
  73. var $local_timezone = NULL;
  74. // Use if the db timezone is stored in a field.
  75. var $db_timezone_field = NULL;
  76. // Use if the local timezone is stored in a field.
  77. var $local_timezone_field = NULL;
  78. // Use if the offset is stored in a field.
  79. var $offset_field = NULL;
  80. /**
  81. * The object constuctor.
  82. */
  83. function __construct($date_type = DATE_DATETIME, $local_timezone = NULL, $offset = '+00:00') {
  84. $this->db_type = Database::getConnection()->databaseType();
  85. $this->date_type = $date_type;
  86. $this->db_timezone = 'UTC';
  87. $this->local_timezone = isset($local_timezone) ? $local_timezone : date_default_timezone();
  88. $this->set_db_timezone($offset);
  89. }
  90. /**
  91. * See if the db has timezone name support.
  92. */
  93. function db_tz_support($reset = FALSE) {
  94. $has_support = variable_get('date_db_tz_support', -1);
  95. if ($has_support == -1 || $reset) {
  96. $has_support = FALSE;
  97. switch ($this->db_type) {
  98. case 'mysql':
  99. $test = db_query("SELECT CONVERT_TZ('2008-02-15 12:00:00', 'UTC', 'US/Central')")->fetchField();
  100. if ($test == '2008-02-15 06:00:00') {
  101. $has_support = TRUE;
  102. }
  103. break;
  104. case 'pgsql':
  105. $test = db_query("SELECT '2008-02-15 12:00:00 UTC' AT TIME ZONE 'US/Central'")->fetchField();
  106. if ($test == '2008-02-15 06:00:00') {
  107. $has_support = TRUE;
  108. }
  109. break;
  110. }
  111. variable_set('date_db_tz_support', $has_support);
  112. }
  113. return $has_support;
  114. }
  115. /**
  116. * Set the database timzone offset.
  117. *
  118. * Setting the db timezone to UTC is done to ensure consistency in date
  119. * handling whether or not the database can do proper timezone conversion.
  120. *
  121. * Views filters that not exposed are cached and won't set the timezone
  122. * so views date filters should add 'cacheable' => 'no' to their
  123. * definitions to ensure that the database timezone gets set properly
  124. * when the query is executed.
  125. *
  126. * @param string $offset
  127. * An offset value to set the database timezone to. This will only
  128. * set a fixed offset, not a timezone, so any value other than
  129. * '+00:00' should be used with caution.
  130. */
  131. function set_db_timezone($offset = '+00:00') {
  132. static $already_set = FALSE;
  133. $type = Database::getConnection()->databaseType();
  134. if (!$already_set) {
  135. switch ($type) {
  136. case 'mysql':
  137. db_query("SET @@session.time_zone = '$offset'");
  138. break;
  139. case 'pgsql':
  140. db_query("SET TIME ZONE INTERVAL '$offset' HOUR TO MINUTE");
  141. break;
  142. case 'sqlsrv':
  143. // Issue #1201342, This is the wrong way to set the timezone, this
  144. // still needs to be fixed. In the meantime, commenting this out makes
  145. // SQLSRV functional.
  146. // db_query('TimeZone.setDefault(TimeZone.getTimeZone("GMT"))');
  147. break;
  148. }
  149. $already_set = TRUE;
  150. }
  151. }
  152. /**
  153. * Return timezone offset for the date being processed.
  154. */
  155. function get_offset($comp_date = NULL) {
  156. if (!empty($this->db_timezone) && !empty($this->local_timezone)) {
  157. if ($this->db_timezone != $this->local_timezone) {
  158. if (empty($comp_date)) {
  159. $comp_date = date_now($this->db_timezone);
  160. }
  161. $comp_date->setTimezone(timezone_open($this->local_timezone));
  162. return date_offset_get($comp_date);
  163. }
  164. }
  165. return 0;
  166. }
  167. /**
  168. * Helper function to create cross-database SQL dates.
  169. *
  170. * @param string $field
  171. * The real table and field name, like 'tablename.fieldname' .
  172. * @param string $offset
  173. * The name of a field that holds the timezone offset or an
  174. * offset value. If NULL, the normal Drupal timezone handling
  175. * will be used, if $offset = 0 no adjustment will be made.
  176. *
  177. * @return string
  178. * An appropriate SQL string for the db type and field type.
  179. */
  180. function sql_field($field, $offset = NULL, $comp_date = NULL) {
  181. if (strtoupper($field) == 'NOW') {
  182. // NOW() will be in UTC since that is what we set the db timezone to.
  183. $this->local_timezone = 'UTC';
  184. return $this->sql_offset('NOW()', $offset);
  185. }
  186. switch ($this->db_type) {
  187. case 'mysql':
  188. switch ($this->date_type) {
  189. case DATE_UNIX:
  190. $field = "FROM_UNIXTIME($field)";
  191. break;
  192. case DATE_ISO:
  193. $field = "STR_TO_DATE($field, '%Y-%m-%dT%T')";
  194. break;
  195. case DATE_DATETIME:
  196. break;
  197. }
  198. break;
  199. case 'pgsql':
  200. switch ($this->date_type) {
  201. case DATE_UNIX:
  202. $field = "$field::ABSTIME";
  203. break;
  204. case DATE_ISO:
  205. $field = "TO_DATE($field, 'FMYYYY-FMMM-FMDDTFMHH24:FMMI:FMSS')";
  206. break;
  207. case DATE_DATETIME:
  208. break;
  209. }
  210. break;
  211. case 'sqlite':
  212. switch ($this->date_type) {
  213. case DATE_UNIX:
  214. $field = "datetime($field, 'unixepoch')";
  215. break;
  216. case DATE_ISO:
  217. case DATE_DATETIME:
  218. $field = "datetime($field)";
  219. break;
  220. }
  221. break;
  222. case 'sqlsrv':
  223. switch ($this->date_type) {
  224. case DATE_UNIX:
  225. $field = "DATEADD(s, $field, '19700101 00:00:00:000')";
  226. break;
  227. case DATE_ISO:
  228. case DATE_DATETIME:
  229. $field = "CAST($field as smalldatetime)";
  230. break;
  231. }
  232. break;
  233. break;
  234. }
  235. // Adjust the resulting value to the right timezone/offset.
  236. return $this->sql_tz($field, $offset, $comp_date);
  237. }
  238. /**
  239. * Adjust a field value by an offset in seconds.
  240. */
  241. function sql_offset($field, $offset = NULL) {
  242. if (!empty($offset)) {
  243. switch ($this->db_type) {
  244. case 'mysql':
  245. return "ADDTIME($field, SEC_TO_TIME($offset))";
  246. case 'pgsql':
  247. return "($field + INTERVAL '$offset SECONDS')";;
  248. case 'sqlite':
  249. return "datetime($field, '$offset seconds')";
  250. case 'sqlsrv':
  251. return "DATEADD(second, $offset, $field)";
  252. }
  253. }
  254. return $field;
  255. }
  256. /**
  257. * Adjusts a field value by time interval.
  258. *
  259. * @param string $field
  260. * The field to be adjusted.
  261. * @param string $direction
  262. * Either ADD or SUB.
  263. * @param int $count
  264. * The number of values to adjust.
  265. * @param string $granularity
  266. * The granularity of the adjustment, should be singular,
  267. * like SECOND, MINUTE, DAY, HOUR.
  268. */
  269. function sql_date_math($field, $direction, $count, $granularity) {
  270. $granularity = strtoupper($granularity);
  271. switch ($this->db_type) {
  272. case 'mysql':
  273. switch ($direction) {
  274. case 'ADD':
  275. return "DATE_ADD($field, INTERVAL $count $granularity)";
  276. case 'SUB':
  277. return "DATE_SUB($field, INTERVAL $count $granularity)";
  278. }
  279. case 'pgsql':
  280. $granularity .= 'S';
  281. switch ($direction) {
  282. case 'ADD':
  283. return "($field + INTERVAL '$count $granularity')";
  284. case 'SUB':
  285. return "($field - INTERVAL '$count $granularity')";
  286. }
  287. case 'sqlite':
  288. $granularity .= 'S';
  289. switch ($direction) {
  290. case 'ADD':
  291. return "datetime($field, '+$count $granularity')";
  292. case 'SUB':
  293. return "datetime($field, '-$count $granularity')";
  294. }
  295. }
  296. return $field;
  297. }
  298. /**
  299. * Select a date value from the database, adjusting the value
  300. * for the timezone.
  301. *
  302. * Check whether database timezone conversion is supported in
  303. * this system and use it if possible, otherwise use an
  304. * offset.
  305. *
  306. * @param string $field
  307. * The field to be adjusted.
  308. * @param bool $offset
  309. * Set a fixed offset or offset field to use for the date.
  310. * If set, no timezone conversion will be done and the
  311. * offset will be used.
  312. */
  313. function sql_tz($field, $offset = NULL, $comp_date = NULL) {
  314. // If the timezones are values they need to be quoted, but
  315. // if they are field names they do not.
  316. $db_zone = !empty($this->db_timezone_field) ? $this->db_timezone_field : "'{$this->db_timezone}'";
  317. $localzone = !empty($this->local_timezone_field) ? $this->local_timezone_field : "'{$this->local_timezone}'";
  318. // If a fixed offset is required, use it.
  319. if ($offset !== NULL) {
  320. return $this->sql_offset($field, $offset);
  321. }
  322. // If the db and local timezones are the same, make no adjustment.
  323. elseif ($db_zone == $localzone) {
  324. return $this->sql_offset($field, 0);
  325. }
  326. // If the db has no timezone support, adjust by the offset,
  327. // could be either a field name or a value.
  328. elseif (!$this->db_tz_support() || empty($localzone)) {
  329. if (!empty($this->offset_field)) {
  330. return $this->sql_offset($field, $this->offset_field);
  331. }
  332. else {
  333. return $this->sql_offset($field, $this->get_offset($comp_date));
  334. }
  335. }
  336. // Otherwise make a database timezone adjustment to the field.
  337. else {
  338. switch ($this->db_type) {
  339. case 'mysql':
  340. return "CONVERT_TZ($field, $db_zone, $localzone)";
  341. case 'pgsql':
  342. // WITH TIME ZONE assumes the date is using the system
  343. // timezone, which should have been set to UTC.
  344. return "$field::timestamp with time zone AT TIME ZONE $localzone";
  345. }
  346. }
  347. }
  348. /**
  349. * Helper function to create cross-database SQL date formatting.
  350. *
  351. * @param string $format
  352. * A format string for the result, like 'Y-m-d H:i:s' .
  353. * @param string $field
  354. * The real table and field name, like 'tablename.fieldname' .
  355. *
  356. * @return string
  357. * An appropriate SQL string for the db type and field type.
  358. */
  359. function sql_format($format, $field) {
  360. switch ($this->db_type) {
  361. case 'mysql':
  362. $replace = array(
  363. 'Y' => '%Y',
  364. 'y' => '%y',
  365. 'M' => '%b',
  366. 'm' => '%m',
  367. 'n' => '%c',
  368. 'F' => '%M',
  369. 'D' => '%a',
  370. 'd' => '%d',
  371. 'l' => '%W',
  372. 'j' => '%e',
  373. 'W' => '%v',
  374. 'H' => '%H',
  375. 'h' => '%h',
  376. 'i' => '%i',
  377. 's' => '%s',
  378. 'A' => '%p',
  379. '\WW' => 'W%U',
  380. );
  381. $format = strtr($format, $replace);
  382. return "DATE_FORMAT($field, '$format')";
  383. case 'pgsql':
  384. $replace = array(
  385. 'Y' => 'YYYY',
  386. 'y' => 'YY',
  387. 'M' => 'Mon',
  388. 'm' => 'MM',
  389. // No format for Numeric representation of a month, without leading
  390. // zeros.
  391. 'n' => 'MM',
  392. 'F' => 'Month',
  393. 'D' => 'Dy',
  394. 'd' => 'DD',
  395. 'l' => 'Day',
  396. // No format for Day of the month without leading zeros.
  397. 'j' => 'DD',
  398. 'W' => 'WW',
  399. 'H' => 'HH24',
  400. 'h' => 'HH12',
  401. 'i' => 'MI',
  402. 's' => 'SS',
  403. 'A' => 'AM',
  404. '\T' => '"T"',
  405. // '\W' => // TODO, what should this be?
  406. );
  407. $format = strtr($format, $replace);
  408. return "TO_CHAR($field, '$format')";
  409. case 'sqlite':
  410. $replace = array(
  411. // 4 digit year number.
  412. 'Y' => '%Y',
  413. // No format for 2 digit year number.
  414. 'y' => '%Y',
  415. // No format for 3 letter month name.
  416. 'M' => '%m',
  417. // Month number with leading zeros.
  418. 'm' => '%m',
  419. // No format for month number without leading zeros.
  420. 'n' => '%m',
  421. // No format for full month name.
  422. 'F' => '%m',
  423. // No format for 3 letter day name.
  424. 'D' => '%d',
  425. // Day of month number with leading zeros.
  426. 'd' => '%d',
  427. // No format for full day name.
  428. 'l' => '%d',
  429. // No format for day of month number without leading zeros.
  430. 'j' => '%d',
  431. // ISO week number.
  432. 'W' => '%W',
  433. // 24 hour hour with leading zeros.
  434. 'H' => '%H',
  435. // No format for 12 hour hour with leading zeros.
  436. 'h' => '%H',
  437. // Minutes with leading zeros.
  438. 'i' => '%M',
  439. // Seconds with leading zeros.
  440. 's' => '%S',
  441. // No format for AM/PM.
  442. 'A' => '',
  443. // Week number.
  444. '\WW' => '',
  445. );
  446. $format = strtr($format, $replace);
  447. return "strftime('$format', $field)";
  448. case 'sqlsrv':
  449. $replace = array(
  450. // 4 digit year number.
  451. 'Y' => "' + CAST(DATEPART(year, $field) AS nvarchar) + '",
  452. // 2 digit year number.
  453. 'y' => "' + RIGHT(DATEPART(year, $field), 2) + '",
  454. // 3 letter month name.
  455. 'M' => "' + LEFT(DATENAME(month, $field), 3) + '",
  456. // Month number with leading zeros.
  457. 'm' => "' + RIGHT('0' + CAST(DATEPART(month, $field) AS nvarchar), 2) + '",
  458. // Month number without leading zeros.
  459. 'n' => "' + CAST(DATEPART(month, $field) AS nvarchar) + '",
  460. // Full month name.
  461. 'F' => "' + DATENAME(month, $field) + '",
  462. // 3 letter day name.
  463. 'D' => "' + LEFT(DATENAME(day, $field), 3) + '",
  464. // Day of month number with leading zeros.
  465. 'd' => "' + RIGHT('0' + CAST(DATEPART(day, $field) AS nvarchar), 2) + '",
  466. // Full day name.
  467. 'l' => "' + DATENAME(day, $field) + '",
  468. // Day of month number without leading zeros.
  469. 'j' => "' + CAST(DATEPART(day, $field) AS nvarchar) + '",
  470. // ISO week number.
  471. 'W' => "' + CAST(DATEPART(iso_week, $field) AS nvarchar) + '",
  472. // 24 hour with leading zeros.
  473. 'H' => "' + RIGHT('0' + CAST(DATEPART(hour, $field) AS nvarchar), 2) + '",
  474. // 12 hour with leading zeros.
  475. // Conversion to 'mon dd yyyy hh:miAM/PM' format (corresponds to style
  476. // 100 in MSSQL).
  477. // Hour position is fixed, so we use SUBSTRING to extract it.
  478. 'h' => "' + RIGHT('0' + LTRIM(SUBSTRING(CONVERT(nvarchar, $field, 100), 13, 2)), 2) + '",
  479. // Minutes with leading zeros.
  480. 'i' => "' + RIGHT('0' + CAST(DATEPART(minute, $field) AS nvarchar), 2) + '",
  481. // Seconds with leading zeros.
  482. 's' => "' + RIGHT('0' + CAST(DATEPART(second, $field) AS nvarchar), 2) + '",
  483. // AM/PM.
  484. // Conversion to 'mon dd yyyy hh:miAM/PM' format (corresponds to style
  485. // 100 in MSSQL).
  486. 'A' => "' + RIGHT(CONVERT(nvarchar, $field, 100), 2) + '",
  487. // Week number.
  488. '\WW' => "' + CAST(DATEPART(week, $field) AS nvarchar) + '",
  489. '\T' => 'T',
  490. // MS SQL uses single quote as escape symbol.
  491. '\'' => '\'\'',
  492. );
  493. $format = strtr($format, $replace);
  494. $format = "'$format'";
  495. return $format;
  496. }
  497. }
  498. /**
  499. * Helper function to create cross-database SQL date extraction.
  500. *
  501. * @param string $extract_type
  502. * The type of value to extract from the date, like 'MONTH'.
  503. * @param string $field
  504. * The real table and field name, like 'tablename.fieldname'.
  505. *
  506. * @return string
  507. * An appropriate SQL string for the db type and field type.
  508. */
  509. function sql_extract($extract_type, $field) {
  510. // Note there is no space after FROM to avoid db_rewrite problems
  511. // see http://drupal.org/node/79904.
  512. switch (strtoupper($extract_type)) {
  513. case 'DATE':
  514. return $field;
  515. case 'YEAR':
  516. return "EXTRACT(YEAR FROM($field))";
  517. case 'MONTH':
  518. return "EXTRACT(MONTH FROM($field))";
  519. case 'DAY':
  520. return "EXTRACT(DAY FROM($field))";
  521. case 'HOUR':
  522. return "EXTRACT(HOUR FROM($field))";
  523. case 'MINUTE':
  524. return "EXTRACT(MINUTE FROM($field))";
  525. case 'SECOND':
  526. return "EXTRACT(SECOND FROM($field))";
  527. // ISO week number for date.
  528. case 'WEEK':
  529. switch ($this->db_type) {
  530. case 'mysql':
  531. // WEEK using arg 3 in MySQl should return the same value as
  532. // Postgres EXTRACT.
  533. return "WEEK($field, 3)";
  534. case 'pgsql':
  535. return "EXTRACT(WEEK FROM($field))";
  536. }
  537. case 'DOW':
  538. switch ($this->db_type) {
  539. case 'mysql':
  540. // MySQL returns 1 for Sunday through 7 for Saturday, PHP date
  541. // functions and Postgres use 0 for Sunday and 6 for Saturday.
  542. return "INTEGER(DAYOFWEEK($field) - 1)";
  543. case 'pgsql':
  544. return "EXTRACT(DOW FROM($field))";
  545. }
  546. case 'DOY':
  547. switch ($this->db_type) {
  548. case 'mysql':
  549. return "DAYOFYEAR($field)";
  550. case 'pgsql':
  551. return "EXTRACT(DOY FROM($field))";
  552. }
  553. }
  554. }
  555. /**
  556. * Creates a where clause to compare a complete date field to a date value.
  557. *
  558. * @param string $type
  559. * The type of value we're comparing to, could be another field
  560. * or a date value.
  561. * @param string $field
  562. * The db table and field name, like "$table.$field".
  563. * @param string $operator
  564. * The db comparison operator to use, like '='.
  565. * @param int $value
  566. * The value to compare the extracted date part to, could be a field name or
  567. * a date string or NOW().
  568. *
  569. * @return string
  570. * SQL for the where clause for this operation.
  571. */
  572. function sql_where_date($type, $field, $operator, $value, $adjustment = NULL) {
  573. $type = strtoupper($type);
  574. if (strtoupper($value) == 'NOW') {
  575. $value = $this->sql_field('NOW', $adjustment);
  576. }
  577. elseif ($type == 'FIELD') {
  578. $value = $this->sql_field($value, $adjustment);
  579. }
  580. elseif ($type == 'DATE') {
  581. $date = new DateObject($value, date_default_timezone(), DATE_FORMAT_DATETIME);
  582. if (!empty($adjustment)) {
  583. date_modify($date, $adjustment . ' seconds');
  584. }
  585. // When comparing a field to a date we can avoid doing timezone
  586. // conversion by altering the comparison date to the db timezone.
  587. // This won't work if the timezone is a field instead of a value.
  588. if (empty($this->db_timezone_field) && empty($this->local_timezone_field) && $this->db_timezone_field != $this->local_timezone_field) {
  589. $date->setTimezone(timezone_open($this->db_timezone));
  590. $this->local_timezone = $this->db_timezone;
  591. }
  592. $value = "'" . $date->format(DATE_FORMAT_DATETIME, TRUE) . "'";
  593. }
  594. if ($this->local_timezone != $this->db_timezone) {
  595. $field = $this->sql_field($field);
  596. }
  597. else {
  598. $field = $this->sql_field($field, 0);
  599. }
  600. return "$field $operator $value";
  601. }
  602. /**
  603. * Creates a where clause comparing an extracted date part to an integer.
  604. *
  605. * @param string $part
  606. * The part to extract, YEAR, MONTH, DAY, etc.
  607. * @param string $field
  608. * The db table and field name, like "$table.$field".
  609. * @param string $operator
  610. * The db comparison operator to use, like '=' .
  611. * @param int $value
  612. * The integer value to compare the extracted date part to.
  613. *
  614. * @return string
  615. * SQL for the where clause for this operation.
  616. */
  617. function sql_where_extract($part, $field, $operator, $value, $adjustment = NULL) {
  618. if (empty($adjustment) && $this->local_timezone != $this->db_timezone) {
  619. $field = $this->sql_field($field);
  620. }
  621. else {
  622. $field = $this->sql_field($field, $adjustment);
  623. }
  624. return $this->sql_extract($part, $field) . " $operator $value";
  625. }
  626. /**
  627. * Create a where clause to compare a formated field to a formated value.
  628. *
  629. * @param string $format
  630. * The format to use on the date and the value when comparing them.
  631. * @param string $field
  632. * The db table and field name, like "$table.$field".
  633. * @param string $operator
  634. * The db comparison operator to use, like '=' .
  635. * @param string $value
  636. * The value to compare the extracted date part to, could be a
  637. * field name or a date string or NOW().
  638. *
  639. * @return string
  640. * SQL for the where clause for this operation.
  641. */
  642. function sql_where_format($format, $field, $operator, $value, $adjustment = NULL) {
  643. if (empty($adjustment) && $this->local_timezone != $this->db_timezone) {
  644. $field = $this->sql_field($field);
  645. }
  646. else {
  647. $field = $this->sql_field($field, $adjustment);
  648. }
  649. return $this->sql_format($format, $field) . " $operator '$value'";
  650. }
  651. /**
  652. * An array of all date parts,
  653. * optionally limited to an array of allowed parts.
  654. */
  655. function date_parts($limit = NULL) {
  656. $parts = array(
  657. 'year' => t('Year', array(), array('context' => 'datetime')),
  658. 'month' => t('Month', array(), array('context' => 'datetime')),
  659. 'day' => t('Day', array(), array('context' => 'datetime')),
  660. 'hour' => t('Hour', array(), array('context' => 'datetime')),
  661. 'minute' => t('Minute', array(), array('context' => 'datetime')),
  662. 'second' => t('Second', array(), array('context' => 'datetime')),
  663. );
  664. if (!empty($limit)) {
  665. $last = FALSE;
  666. foreach ($parts as $key => $part) {
  667. if ($last) {
  668. unset($parts[$key]);
  669. }
  670. if ($key == $limit) {
  671. $last = TRUE;
  672. }
  673. }
  674. }
  675. return $parts;
  676. }
  677. /**
  678. * Part information.
  679. *
  680. * @param string $op
  681. * 'min', 'max', 'format', 'sep', 'empty_now', 'empty_min', 'empty_max' .
  682. * Returns all info if empty.
  683. * @param string $part
  684. * 'year', 'month', 'day', 'hour', 'minute', or 'second.
  685. * returns info for all parts if empty.
  686. */
  687. function part_info($op = NULL, $part = NULL) {
  688. $info = array();
  689. $info['min'] = array(
  690. 'year' => 100,
  691. 'month' => 1,
  692. 'day' => 1,
  693. 'hour' => 0,
  694. 'minute' => 0,
  695. 'second' => 0,
  696. );
  697. $info['max'] = array(
  698. 'year' => 4000,
  699. 'month' => 12,
  700. 'day' => 31,
  701. 'hour' => 23,
  702. 'minute' => 59,
  703. 'second' => 59,
  704. );
  705. $info['format'] = array(
  706. 'year' => 'Y',
  707. 'month' => 'm',
  708. 'day' => 'd',
  709. 'hour' => 'H',
  710. 'minute' => 'i',
  711. 'second' => 's',
  712. );
  713. $info['sep'] = array(
  714. 'year' => '',
  715. 'month' => '-',
  716. 'day' => '-',
  717. 'hour' => ' ',
  718. 'minute' => ':',
  719. 'second' => ':',
  720. );
  721. $info['empty_now'] = array(
  722. 'year' => date('Y'),
  723. 'month' => date('m'),
  724. 'day' => min('28', date('d')),
  725. 'hour' => date('H'),
  726. 'minute' => date('i'),
  727. 'second' => date('s'),
  728. );
  729. $info['empty_min'] = array(
  730. 'year' => '1000',
  731. 'month' => '01',
  732. 'day' => '01',
  733. 'hour' => '00',
  734. 'minute' => '00',
  735. 'second' => '00',
  736. );
  737. $info['empty_max'] = array(
  738. 'year' => '9999',
  739. 'month' => '12',
  740. 'day' => '31',
  741. 'hour' => '23',
  742. 'minute' => '59',
  743. 'second' => '59',
  744. );
  745. if (!empty($op)) {
  746. if (!empty($part)) {
  747. return $info[$op][$part];
  748. }
  749. else {
  750. return $info[$op];
  751. }
  752. }
  753. return $info;
  754. }
  755. /**
  756. * Create a complete datetime value out of an
  757. * incomplete array of selected values.
  758. *
  759. * For example, array('year' => 2008, 'month' => 05) will fill
  760. * in the day, hour, minute and second with the earliest possible
  761. * values if type = 'min', the latest possible values if type = 'max',
  762. * and the current values if type = 'now' .
  763. */
  764. function complete_date($selected, $type = 'now') {
  765. if (empty($selected)) {
  766. return '';
  767. }
  768. // Special case for weeks.
  769. if (array_key_exists('week', $selected)) {
  770. $dates = date_week_range($selected['week'], $selected['year']);
  771. switch ($type) {
  772. case 'empty_now':
  773. case 'empty_min':
  774. case 'min':
  775. return date_format($dates[0], 'Y-m-d H:i:s');
  776. case 'empty_max':
  777. case 'max':
  778. return date_format($dates[1], 'Y-m-d H:i:s');
  779. default:
  780. return;
  781. }
  782. }
  783. $compare = array_merge($this->part_info('empty_' . $type), $selected);
  784. // If this is a max date, make sure the last day of
  785. // the month is the right one for this date.
  786. if ($type == 'max') {
  787. $compare['day'] = date_days_in_month($compare['year'], $compare['month']);
  788. }
  789. $value = '';
  790. $separators = $this->part_info('sep');
  791. foreach ($this->date_parts() as $key => $name) {
  792. $value .= $separators[$key] . (!empty($selected[$key]) ? $selected[$key] : $compare[$key]);
  793. }
  794. return $value;
  795. }
  796. /**
  797. * Converts a format string into help text, i.e. 'Y-m-d' becomes 'YYYY-MM-DD'.
  798. *
  799. * @param string $format
  800. * A date format string.
  801. *
  802. * @return string
  803. * The conveted help text.
  804. */
  805. function format_help($format) {
  806. $replace = array(
  807. 'Y' => 'YYYY',
  808. 'm' => 'MM',
  809. 'd' => 'DD',
  810. 'H' => 'HH',
  811. 'i' => 'MM',
  812. 's' => 'SS',
  813. '\T' => 'T',
  814. );
  815. return strtr($format, $replace);
  816. }
  817. /**
  818. * A function to test the validity of various date parts
  819. */
  820. function part_is_valid($value, $type) {
  821. if (!preg_match('/^[0-9]*$/', $value)) {
  822. return FALSE;
  823. }
  824. $value = intval($value);
  825. if ($value <= 0) {
  826. return FALSE;
  827. }
  828. switch ($type) {
  829. case 'year':
  830. if ($value < DATE_MIN_YEAR) {
  831. return FALSE;
  832. }
  833. break;
  834. case 'month':
  835. if ($value < 0 || $value > 12) {
  836. return FALSE;
  837. }
  838. break;
  839. case 'day':
  840. if ($value < 0 || $value > 31) {
  841. return FALSE;
  842. }
  843. break;
  844. case 'week':
  845. if ($value < 0 || $value > 53) {
  846. return FALSE;
  847. }
  848. break;
  849. }
  850. return TRUE;
  851. }
  852. /**
  853. * @todo.
  854. */
  855. function views_formats($granularity, $type = 'sql') {
  856. if (empty($granularity)) {
  857. return DATE_FORMAT_ISO;
  858. }
  859. $formats = array('display', 'sql');
  860. // Start with the site long date format and add seconds to it.
  861. $short = str_replace(':i', ':i:s', variable_get('date_format_short', 'l, F j, Y - H:i'));
  862. switch ($granularity) {
  863. case 'year':
  864. $formats['display'] = 'Y';
  865. $formats['sql'] = 'Y';
  866. break;
  867. case 'month':
  868. $formats['display'] = date_limit_format($short, array('year', 'month'));
  869. $formats['sql'] = 'Y-m';
  870. break;
  871. case 'day':
  872. $formats['display'] = date_limit_format($short, array('year', 'month', 'day'));
  873. $formats['sql'] = 'Y-m-d';
  874. break;
  875. case 'hour':
  876. $formats['display'] = date_limit_format($short, array('year', 'month', 'day', 'hour'));
  877. $formats['sql'] = 'Y-m-d\TH';
  878. break;
  879. case 'minute':
  880. $formats['display'] = date_limit_format($short, array('year', 'month', 'day', 'hour', 'minute'));
  881. $formats['sql'] = 'Y-m-d\TH:i';
  882. break;
  883. case 'second':
  884. $formats['display'] = date_limit_format($short, array('year', 'month', 'day', 'hour', 'minute', 'second'));
  885. $formats['sql'] = 'Y-m-d\TH:i:s';
  886. break;
  887. case 'week':
  888. $formats['display'] = 'F j Y (W)';
  889. $formats['sql'] = 'Y-\WW';
  890. break;
  891. }
  892. return $formats[$type];
  893. }
  894. /**
  895. * @todo.
  896. */
  897. function granularity_form($granularity) {
  898. $form = array(
  899. '#title' => t('Granularity'),
  900. '#type' => 'radios',
  901. '#default_value' => $granularity,
  902. '#options' => $this->date_parts(),
  903. );
  904. return $form;
  905. }
  906. /**
  907. * Parse date parts from an ISO date argument.
  908. *
  909. * Based on ISO 8601 date duration and time interval standards.
  910. *
  911. * Parses a value like 2006-01-01--2006-01-15, or 2006-W24, or @P1W.
  912. * Separate start and end dates or date and period with a double hyphen (--).
  913. *
  914. * The 'end' portion of the argument can be eliminated if it is the same as
  915. * the 'start' portion. Use @ instead of a date to substitute in the current
  916. * date and time.
  917. *
  918. * Use periods (P1H, P1D, P1W, P1M, P1Y) to get next hour/day/week/month/year
  919. * from now. Use date before P sign to get next hour/day/week/month/year from
  920. * that date. Use period then date to get a period that ends on the date.
  921. *
  922. * @see http://en.wikipedia.org/wiki/ISO_8601#Week_dates
  923. * @see http://en.wikipedia.org/wiki/ISO_8601#Duration
  924. */
  925. function arg_parts($argument) {
  926. $values = array();
  927. // Keep mal-formed arguments from creating errors.
  928. if (empty($argument) || is_array($argument)) {
  929. return array('date' => array(), 'period' => array());
  930. }
  931. $fromto = explode('--', $argument);
  932. foreach ($fromto as $arg) {
  933. $parts = array();
  934. if ($arg == '@') {
  935. $date = date_now();
  936. $parts['date'] = $date->toArray();
  937. }
  938. elseif (preg_match('/(\d{4})?-?(W)?(\d{1,2})?-?(\d{1,2})?[T\s]?(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?/', $arg, $matches)) {
  939. $date = array();
  940. if (!empty($matches[1])) {
  941. $date['year'] = $matches[1];
  942. }
  943. if (!empty($matches[3])) {
  944. if (empty($matches[2])) {
  945. $date['month'] = $matches[3];
  946. }
  947. else {
  948. $date['week'] = $matches[3];
  949. }
  950. }
  951. if (!empty($matches[4])) {
  952. $date['day'] = $matches[4];
  953. }
  954. if (!empty($matches[5])) {
  955. $date['hour'] = $matches[5];
  956. }
  957. if (!empty($matches[6])) {
  958. $date['minute'] = $matches[6];
  959. }
  960. if (!empty($matches[7])) {
  961. $date['second'] = $matches[7];
  962. }
  963. $parts['date'] = $date;
  964. }
  965. if (preg_match('/^P(\d{1,4}[Y])?(\d{1,2}[M])?(\d{1,2}[W])?(\d{1,2}[D])?([T]{0,1})?(\d{1,2}[H])?(\d{1,2}[M])?(\d{1,2}[S])?/', $arg, $matches)) {
  966. $period = array();
  967. if (!empty($matches[1])) {
  968. $period['year'] = str_replace('Y', '', $matches[1]);
  969. }
  970. if (!empty($matches[2])) {
  971. $period['month'] = str_replace('M', '', $matches[2]);
  972. }
  973. if (!empty($matches[3])) {
  974. $period['week'] = str_replace('W', '', $matches[3]);
  975. }
  976. if (!empty($matches[4])) {
  977. $period['day'] = str_replace('D', '', $matches[4]);
  978. }
  979. if (!empty($matches[6])) {
  980. $period['hour'] = str_replace('H', '', $matches[6]);
  981. }
  982. if (!empty($matches[7])) {
  983. $period['minute'] = str_replace('M', '', $matches[7]);
  984. }
  985. if (!empty($matches[8])) {
  986. $period['second'] = str_replace('S', '', $matches[8]);
  987. }
  988. $parts['period'] = $period;
  989. }
  990. $values[] = $parts;
  991. }
  992. return $values;
  993. }
  994. /**
  995. * Convert strings like '+1 day' to the ISO equivalent, like 'P1D' .
  996. */
  997. function arg_replace($arg) {
  998. if (!preg_match('/([+|-])\s?([0-9]{1,32})\s?([day(s)?|week(s)?|month(s)?|year(s)?|hour(s)?|minute(s)?|second(s)?]{1,10})/', $arg, $results)) {
  999. return str_replace('now', '@', $arg);
  1000. }
  1001. $direction = $results[1];
  1002. $count = $results[2];
  1003. $item = $results[3];
  1004. $replace = array(
  1005. 'now' => '@',
  1006. '+' => 'P',
  1007. '-' => 'P-',
  1008. 'years' => 'Y',
  1009. 'year' => 'Y',
  1010. 'months' => 'M',
  1011. 'month' => 'M',
  1012. 'weeks' => 'W',
  1013. 'week' => 'W',
  1014. 'days' => 'D',
  1015. 'day' => 'D',
  1016. 'hours' => 'H',
  1017. 'hour' => 'H',
  1018. 'minutes' => 'M',
  1019. 'minute' => 'M',
  1020. 'seconds' => 'S',
  1021. 'second' => 'S',
  1022. ' ' => '',
  1023. ' ' => '',
  1024. );
  1025. $prefix = in_array($item, array('hours', 'hour', 'minutes', 'minute', 'seconds', 'second')) ? 'T' : '';
  1026. return $prefix . strtr($direction, $replace) . $count . strtr($item, $replace);
  1027. }
  1028. /**
  1029. * Use the parsed values from the ISO argument to determine the
  1030. * granularity of this period.
  1031. */
  1032. function arg_granularity($arg) {
  1033. $granularity = '';
  1034. $parts = $this->arg_parts($arg);
  1035. $date = !empty($parts[0]['date']) ? $parts[0]['date'] : (!empty($parts[1]['date']) ? $parts[1]['date'] : array());
  1036. foreach ($date as $key => $part) {
  1037. $granularity = $key;
  1038. }
  1039. return $granularity;
  1040. }
  1041. /**
  1042. * Use the parsed values from the ISO argument to determine the
  1043. * min and max date for this period.
  1044. */
  1045. function arg_range($arg) {
  1046. // Parse the argument to get its parts.
  1047. $parts = $this->arg_parts($arg);
  1048. // Build a range from a period-only argument (assumes the min date is now.)
  1049. if (empty($parts[0]['date']) && !empty($parts[0]['period']) && (empty($parts[1]))) {
  1050. $min_date = date_now();
  1051. $max_date = clone($min_date);
  1052. foreach ($parts[0]['period'] as $part => $value) {
  1053. date_modify($max_date, "+$value $part");
  1054. }
  1055. date_modify($max_date, '-1 second');
  1056. return array($min_date, $max_date);
  1057. }
  1058. // Build a range from a period to period argument.
  1059. if (empty($parts[0]['date']) && !empty($parts[0]['period']) && !empty($parts[1]['period'])) {
  1060. $min_date = date_now();
  1061. $max_date = clone($min_date);
  1062. foreach ($parts[0]['period'] as $part => $value) {
  1063. date_modify($min_date, "+$value $part");
  1064. }
  1065. date_modify($min_date, '-1 second');
  1066. foreach ($parts[1]['period'] as $part => $value) {
  1067. date_modify($max_date, "+$value $part");
  1068. }
  1069. date_modify($max_date, '-1 second');
  1070. return array($min_date, $max_date);
  1071. }
  1072. if (!empty($parts[0]['date'])) {
  1073. $value = $this->complete_date($parts[0]['date'], 'min');
  1074. $min_date = new DateObject($value, date_default_timezone(), DATE_FORMAT_DATETIME);
  1075. // Build a range from a single date-only argument.
  1076. if (empty($parts[1]) || (empty($parts[1]['date']) && empty($parts[1]['period']))) {
  1077. $value = $this->complete_date($parts[0]['date'], 'max');
  1078. $max_date = new DateObject($value, date_default_timezone(), DATE_FORMAT_DATETIME);
  1079. return array($min_date, $max_date);
  1080. }
  1081. // Build a range from start date + period.
  1082. elseif (!empty($parts[1]['period'])) {
  1083. foreach ($parts[1]['period'] as $part => $value) {
  1084. $max_date = clone($min_date);
  1085. date_modify($max_date, "+$value $part");
  1086. }
  1087. date_modify($max_date, '-1 second');
  1088. return array($min_date, $max_date);
  1089. }
  1090. }
  1091. // Build a range from start date and end date.
  1092. if (!empty($parts[1]['date'])) {
  1093. $value = $this->complete_date($parts[1]['date'], 'max');
  1094. $max_date = new DateObject($value, date_default_timezone(), DATE_FORMAT_DATETIME);
  1095. if (isset($min_date)) {
  1096. return array($min_date, $max_date);
  1097. }
  1098. }
  1099. // Build a range from period + end date.
  1100. if (!empty($parts[0]['period'])) {
  1101. $min_date = date_now();
  1102. foreach ($parts[0]['period'] as $part => $value) {
  1103. date_modify($min_date, "$value $part");
  1104. }
  1105. return array($min_date, $max_date);
  1106. }
  1107. // Intercept invalid info and fall back to the current date.
  1108. $now = date_now();
  1109. return array($now, $now);
  1110. }
  1111. }