date_api_sql.inc 36 KB

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