-- ADDDATE()
-- Note: passing in the interval is different
CREATE OR REPLACE FUNCTION adddate(timestamp without time zone, interval)
RETURNS timestamp without time zone AS $$
    SELECT $1 + $2
$$ IMMUTABLE STRICT LANGUAGE SQL;

CREATE OR REPLACE FUNCTION adddate(timestamp without time zone, integer)
RETURNS timestamp without time zone AS $$
  SELECT $1 + (INTERVAL '1 day' * $2)
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- ADDTIME()
-- Note: requires casting if you install both versions
CREATE OR REPLACE FUNCTION addtime(timestamp without time zone, interval)
RETURNS timestamp without time zone AS $$
  SELECT $1 + $2
$$ IMMUTABLE STRICT LANGUAGE SQL;

CREATE OR REPLACE FUNCTION addtime(interval, interval)
RETURNS interval AS $$
  SELECT $1 + $2
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- CONVERT_TZ()
CREATE OR REPLACE FUNCTION convert_tz(timestamp without time zone, text, text)
RETURNS timestamp without time zone AS $$
  SELECT CASE
    WHEN POSITION(':' IN $3) = 0 THEN
      ($1 operator(pg_catalog.||) ' ' operator(pg_catalog.||) $2)::timestamp with time zone AT TIME ZONE $3
    ELSE
      ($1 operator(pg_catalog.||) ' ' operator(pg_catalog.||) $2)::timestamp with time zone AT TIME ZONE $3::interval
    END
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- CURDATE()
CREATE OR REPLACE FUNCTION curdate()
RETURNS date AS $$
  SELECT CURRENT_DATE
$$ VOLATILE LANGUAGE SQL;

-- CURTIME()
CREATE OR REPLACE FUNCTION curtime()
RETURNS time without time zone AS $$
  SELECT LOCALTIME(0)
$$ VOLATILE LANGUAGE SQL;

-- DATEDIFF()
CREATE OR REPLACE FUNCTION datediff(date, date)
RETURNS integer AS $$
  SELECT $1 - $2
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- DATE_ADD()
CREATE OR REPLACE FUNCTION date_add(timestamp without time zone, interval)
RETURNS timestamp without time zone AS $$
  SELECT $1 + $2
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- DATE_FORMAT()
CREATE OR REPLACE FUNCTION date_format(timestamp without time zone, text)
RETURNS text AS $$
  DECLARE
    i int := 1;
    temp text := '';
    c text;
    n text;
    res text;
  BEGIN
    WHILE i <= pg_catalog.length($2) LOOP
      -- Look at current character
      c := SUBSTRING ($2 FROM i FOR 1);
      -- If it's a '%' and not the last character then process it as a placeholder
      IF c = '%' AND i != pg_catalog.length($2) THEN
        n := SUBSTRING ($2 FROM (i + 1) FOR 1);
        SELECT INTO res CASE
          WHEN n = 'a' THEN pg_catalog.to_char($1, 'Dy') 
          WHEN n = 'b' THEN pg_catalog.to_char($1, 'Mon') 
          WHEN n = 'c' THEN pg_catalog.to_char($1, 'FMMM') 
          WHEN n = 'D' THEN pg_catalog.to_char($1, 'FMDDth') 
          WHEN n = 'd' THEN pg_catalog.to_char($1, 'DD') 
          WHEN n = 'e' THEN pg_catalog.to_char($1, 'FMDD') 
          WHEN n = 'f' THEN pg_catalog.to_char($1, 'US') 
          WHEN n = 'H' THEN pg_catalog.to_char($1, 'HH24') 
          WHEN n = 'h' THEN pg_catalog.to_char($1, 'HH12') 
          WHEN n = 'I' THEN pg_catalog.to_char($1, 'HH12')      
          WHEN n = 'i' THEN pg_catalog.to_char($1, 'MI') 
          WHEN n = 'j' THEN pg_catalog.to_char($1, 'DDD') 
          WHEN n = 'k' THEN pg_catalog.to_char($1, 'FMHH24') 
          WHEN n = 'l' THEN pg_catalog.to_char($1, 'FMHH12') 
          WHEN n = 'M' THEN pg_catalog.to_char($1, 'FMMonth') 
          WHEN n = 'm' THEN pg_catalog.to_char($1, 'MM') 
          WHEN n = 'p' THEN pg_catalog.to_char($1, 'AM') 
          WHEN n = 'r' THEN pg_catalog.to_char($1, 'HH12:MI:SS AM') 
          WHEN n = 'S' THEN pg_catalog.to_char($1, 'SS')
          WHEN n = 's' THEN pg_catalog.to_char($1, 'SS')
          WHEN n = 'T' THEN pg_catalog.to_char($1, 'HH24:MI:SS') 
          WHEN n = 'U' THEN pg_catalog.lpad(week($1::date, 0)::text, 2, '0')
          WHEN n = 'u' THEN pg_catalog.lpad(week($1::date, 1)::text, 2, '0')
          WHEN n = 'V' THEN pg_catalog.lpad(week($1::date, 2)::text, 2, '0')
          WHEN n = 'v' THEN pg_catalog.lpad(week($1::date, 3)::text, 2, '0')
          WHEN n = 'W' THEN pg_catalog.to_char($1, 'FMDay') 
          WHEN n = 'w' THEN EXTRACT(DOW FROM $1)::text
          WHEN n = 'X' THEN pg_catalog.lpad(((_calc_week($1::date, _week_mode(2)))[2])::text, 4, '0')
          WHEN n = 'x' THEN pg_catalog.lpad(((_calc_week($1::date, _week_mode(3)))[2])::text, 4, '0')
          WHEN n = 'Y' THEN pg_catalog.to_char($1, 'YYYY') 
          WHEN n = 'y' THEN pg_catalog.to_char($1, 'YY')
          WHEN n = '%' THEN pg_catalog.to_char($1, '%')
          ELSE NULL
        END;
        temp := temp operator(pg_catalog.||) res;
        i := i + 2;
      ELSE
        -- Otherwise just append the character to the string
        temp = temp operator(pg_catalog.||) c;
        i := i + 1;
      END IF; 
    END LOOP;
    RETURN temp;
  END
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- DATE_SUB()
CREATE OR REPLACE FUNCTION date_sub(timestamp without time zone, interval)
RETURNS timestamp without time zone AS $$
  SELECT $1 - $2
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- DAY()
CREATE OR REPLACE FUNCTION day(date)
RETURNS integer AS $$
  SELECT EXTRACT(DAY FROM DATE($1))::integer
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- DAYNAME()
CREATE OR REPLACE FUNCTION dayname(date)
RETURNS text AS $$
  SELECT pg_catalog.to_char($1, 'FMDay')
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- FROM_DAYS()
CREATE OR REPLACE FUNCTION from_days(integer)
RETURNS date AS $$
  SELECT ('0001-01-01 BC'::date + $1 * INTERVAL '1 day')::date
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- FROM_UNIXTIME()
-- Returns local time?  Is this actually the same as MySQL?
-- Depends on: DATE_FORMAT()
CREATE OR REPLACE FUNCTION from_unixtime(bigint)
RETURNS timestamp without time zone AS $$
  SELECT pg_catalog.to_timestamp($1)::timestamp without time zone
$$ IMMUTABLE STRICT LANGUAGE SQL;

CREATE OR REPLACE FUNCTION from_unixtime(bigint, text)
RETURNS text AS $$
  SELECT date_format(from_unixtime($1), $2)
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- GET_FORMAT()
-- Note that first parameter needs to be quoted in this version
CREATE OR REPLACE FUNCTION get_format(text, text)
RETURNS text AS $$
  SELECT CASE
    WHEN $1 ILIKE 'DATE' THEN
      CASE WHEN $2 ILIKE 'USA' THEN '%m.%d.%Y'
      WHEN $2 ILIKE 'JIS' OR $2 ILIKE 'ISO' THEN '%Y-%m-%d'
      WHEN $2 ILIKE 'EUR' THEN '%d.%m.%Y'
      WHEN $2 ILIKE 'INTERNAL' THEN '%Y%m%d'
      ELSE NULL
      END
    WHEN $1 ILIKE 'DATETIME' THEN
      CASE WHEN $2 ILIKE 'USA' OR $2 ILIKE 'EUR' THEN '%Y-%m-%d-%H.%i.%s'
      WHEN $2 ILIKE 'JIS' OR $2 ILIKE 'ISO' THEN '%Y-%m-%d %H:%i:%s'
      WHEN $2 ILIKE 'INTERNAL' THEN '%Y%m%d%H%i%s'
      ELSE NULL
      END
    WHEN $1 ILIKE 'TIME' THEN
      CASE WHEN $2 ILIKE 'USA' THEN '%h:%i:%s %p'
      WHEN $2 ILIKE 'JIS' OR $2 ILIKE 'ISO' THEN '%H:%i:%s'
      WHEN $2 ILIKE 'EUR' THEN 'H.%i.%S'
      WHEN $2 ILIKE 'INTERNAL' THEN '%H%i%s'
      ELSE NULL
      END
    ELSE
      NULL
    END
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- LAST_DAY()
-- Note that for illegal timestamps this function raises an error,
-- whereas under MySQL it returns NULL
CREATE OR REPLACE FUNCTION last_day(timestamp)
RETURNS date AS $$ 
  SELECT CASE
    WHEN EXTRACT(MONTH FROM $1) = 12 THEN
      (((EXTRACT(YEAR FROM $1) + 1) operator(pg_catalog.||) '-01-01')::date - INTERVAL '1 day')::date
    ELSE
      ((EXTRACT(YEAR FROM $1) operator(pg_catalog.||) '-' operator(pg_catalog.||) (EXTRACT(MONTH FROM $1) + 1) operator(pg_catalog.||) '-01')::date - INTERVAL '1 day')::date
    END
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- MAKEDATE()
CREATE OR REPLACE FUNCTION makedate(integer, integer)
RETURNS date AS $$
  SELECT CASE WHEN $2 > 0 THEN
    (($1 operator(pg_catalog.||) '-01-01')::date + ($2 - 1) * INTERVAL '1 day')::date
  ELSE
    NULL
  END
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- MAKETIME()
CREATE OR REPLACE FUNCTION maketime(integer, integer, integer)
RETURNS interval AS $$
  SELECT ($1 operator(pg_catalog.||) ':' operator(pg_catalog.||) $2 operator(pg_catalog.||) ':' operator(pg_catalog.||) $3)::interval
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- MONTHNAME()
CREATE OR REPLACE FUNCTION monthname(date)
RETURNS text AS $$
  SELECT pg_catalog.to_char($1, 'FMMonth')
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- NOW()
-- Part of base PostgreSQL

-- PERIOD_ADD()
CREATE OR REPLACE FUNCTION period_add(integer, integer)
RETURNS text AS $$
  DECLARE
    period text;
    base date;
    baseyear integer;
  BEGIN
      IF pg_catalog.length($1) < 4 THEN
        period := pg_catalog.lpad($1, 4, 0);
      ELSIF pg_catalog.length($1) = 5 THEN
        period := pg_catalog.lpad($1, 6, 0);
      ELSE
        period := $1;
      END IF;

      IF pg_catalog.length(period) = 4 THEN
        baseyear := SUBSTRING(period FROM 1 FOR 2);
        IF baseyear BETWEEN 70 AND 99 THEN
          baseyear := baseyear + 1900;
        ELSE
          baseyear := baseyear + 2000;
        END IF;
        base := (baseyear operator(pg_catalog.||) '-' operator(pg_catalog.||) SUBSTRING(period FROM 3) operator(pg_catalog.||) '-01')::date;
      ELSIF pg_catalog.length(period) = 6 THEN
        base := (SUBSTRING(period FROM 1 FOR 4) operator(pg_catalog.||) '-' operator(pg_catalog.||) SUBSTRING(period FROM 5) operator(pg_catalog.||) '-01')::date;
      ELSE
        RETURN NULL;
      END IF;

      base := base + (INTERVAL '1 month' * $2);
      RETURN pg_catalog.lpad(EXTRACT(YEAR FROM base), 4, '0') operator(pg_catalog.||) pg_catalog.lpad(EXTRACT(MONTH FROM base), 2, '0');
  END
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- PERIOD_DIFF()
CREATE OR REPLACE FUNCTION period_diff(integer, integer)
RETURNS integer AS $$
  DECLARE
    baseyear integer;
    period1 text;
    period2 text;
    months1 integer;
    months2 integer;
  BEGIN
      IF pg_catalog.length($1) < 4 THEN
        period1 := pg_catalog.lpad($1, 4, 0);
      ELSIF pg_catalog.length($1) = 5 THEN
        period1 := pg_catalog.lpad($1, 6, 0);
      ELSE
        period1 := $1;
      END IF;

      IF pg_catalog.length(period1) = 4 THEN
        baseyear := SUBSTRING(period1 FROM 1 FOR 2);
        IF baseyear BETWEEN 70 AND 99 THEN
          baseyear := baseyear + 1900;
        ELSE
          baseyear := baseyear + 2000;
        END IF;
        months1 := baseyear * 12 + SUBSTRING(period1 FROM 3)::integer;
      ELSIF pg_catalog.length(period1) = 6 THEN
        months1 := SUBSTRING(period1 FROM 1 FOR 4)::integer * 12 + SUBSTRING(period1 FROM 5)::integer;
      ELSE
        RETURN NULL;
      END IF;

      IF pg_catalog.length($2) < 4 THEN
        period2 := pg_catalog.lpad($2, 4, 0);
      ELSIF pg_catalog.length($2) = 5 THEN
        period2 := pg_catalog.lpad($2, 6, 0);
      ELSE
        period2 := $2;
      END IF;

      IF pg_catalog.length(period2) = 4 THEN
        baseyear := SUBSTRING(period2 FROM 1 FOR 2);
        IF baseyear BETWEEN 70 AND 99 THEN
          baseyear := baseyear + 1900;
        ELSE
          baseyear := baseyear + 2000;
        END IF;
        months2 := baseyear * 12 + SUBSTRING(period2 FROM 3)::integer;
      ELSIF pg_catalog.length(period2) = 6 THEN
        months2 := SUBSTRING(period2 FROM 1 FOR 4)::integer * 12 + SUBSTRING(period2 FROM 5)::integer;
      ELSE
        RETURN NULL;
      END IF;

      RETURN months1 - months2;
  END
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- SEC_TO_TIME()
CREATE OR REPLACE FUNCTION sec_to_time(bigint)
RETURNS interval AS $$
  SELECT $1 * INTERVAL '1 second'
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- STR_TO_DATE()
-- Note: Doesn't handle weeks of years yet and will return different results
-- to MySQL if you pass in an invalid timestamp
CREATE OR REPLACE FUNCTION str_to_date(text, text)
RETURNS timestamp without time zone AS $$
  DECLARE
    i int := 1;
    temp text := '';
    c text;
    n text; res text;
  BEGIN
    WHILE i <= pg_catalog.length($2) LOOP
      -- Look at current character
      c := SUBSTRING ($2 FROM i FOR 1);
      -- If it's a '%' and not the last character then process it as a placeholder
      IF c = '%' AND i != pg_catalog.length($2) THEN
        n := SUBSTRING ($2 FROM (i + 1) FOR 1);
        SELECT INTO res CASE
          WHEN n = 'a' THEN 'Dy' 
          WHEN n = 'b' THEN 'Mon' 
          WHEN n = 'c' THEN 'FMMM' 
          WHEN n = 'D' THEN 'FMDDth' 
          WHEN n = 'd' THEN 'DD' 
          WHEN n = 'e' THEN 'FMDD' 
          WHEN n = 'f' THEN 'US' 
          WHEN n = 'H' THEN 'HH24' 
          WHEN n = 'h' THEN 'HH12' 
          WHEN n = 'I' THEN 'HH12'      
          WHEN n = 'i' THEN 'MI' 
          WHEN n = 'j' THEN 'DDD' 
          WHEN n = 'k' THEN 'FMHH24' 
          WHEN n = 'l' THEN 'FMHH12' 
          WHEN n = 'M' THEN 'FMMonth' 
          WHEN n = 'm' THEN 'MM' 
          WHEN n = 'p' THEN 'AM' 
          WHEN n = 'r' THEN 'HH12:MI:SS AM' 
          WHEN n = 'S' THEN 'SS'
          WHEN n = 's' THEN 'SS'
          WHEN n = 'T' THEN 'HH24:MI:SS' 
          WHEN n = 'U' THEN '?'
          WHEN n = 'u' THEN '?'
          WHEN n = 'V' THEN '?'
          WHEN n = 'v' THEN '?'
          WHEN n = 'W' THEN 'FMDay' 
          WHEN n = 'w' THEN '?'
          WHEN n = 'X' THEN '?' 
          WHEN n = 'x' THEN '?' 
          WHEN n = 'Y' THEN 'YYYY' 
          WHEN n = 'y' THEN 'YY'
          WHEN n = '%' THEN '%'
          ELSE NULL
        END;
        temp := temp operator(pg_catalog.||) res;
        i := i + 2;
      ELSE
        -- Otherwise just append the character to the string
        temp = temp operator(pg_catalog.||) c;
        i := i + 1;
      END IF; 
    END LOOP;
    RETURN pg_catalog.to_timestamp($1, temp)::timestamp without time zone;
  END
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- SUBDATE()
-- Note: passing in the interval is different
CREATE OR REPLACE FUNCTION subdate(timestamp without time zone, interval)
RETURNS timestamp without time zone AS $$
    SELECT $1 - $2
$$ IMMUTABLE STRICT LANGUAGE SQL;

CREATE OR REPLACE FUNCTION subdate(timestamp without time zone, integer)
RETURNS timestamp without time zone AS $$
  SELECT $1 - (INTERVAL '1 day' * $2)
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- SUBTIME
-- Note: requires casting if you install both versions
CREATE OR REPLACE FUNCTION subtime(timestamp without time zone, interval)
RETURNS timestamp without time zone AS $$
  SELECT $1 - $2
$$ IMMUTABLE STRICT LANGUAGE SQL;

CREATE OR REPLACE FUNCTION subtime(interval, interval)
RETURNS interval AS $$
  SELECT $1 - $2
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- SYSDATE()
CREATE OR REPLACE FUNCTION sysdate()
RETURNS timestamp without time zone AS $$
  SELECT pg_catalog.timeofday()::timestamp(0) without time zone
$$ VOLATILE LANGUAGE SQL;

-- TIME()
-- Not possible to implement

-- TIMEDIFF()
-- Note: requires casting if you install both versions
CREATE OR REPLACE FUNCTION timediff(timestamp without time zone, timestamp without time zone)
RETURNS interval AS $$
  SELECT $1 - $2
$$ IMMUTABLE STRICT LANGUAGE SQL;

CREATE OR REPLACE FUNCTION timediff(time without time zone, time without time zone)
RETURNS interval AS $$
  SELECT $1 - $2
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- TIMESTAMP()
-- Not possible to implement

-- TIMESTAMPADD()
-- Note that first parameter needs to be quoted in this version
CREATE OR REPLACE FUNCTION timestampadd(text, integer, timestamp without time zone)
RETURNS timestamp without time zone AS $$
  SELECT $3 + ($2 operator(pg_catalog.||) ' ' operator(pg_catalog.||) $1)::interval
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- TIMESTAMPDIFF()
-- Note that first parameter needs to be quoted in this version

-- TIME_FORMAT()
CREATE OR REPLACE FUNCTION time_format(interval, text)
RETURNS text AS $$
  DECLARE
    i int := 1;
    temp text := '';
    c text;
    n text;
    res text;
  BEGIN
    WHILE i <= pg_catalog.length($2) LOOP
      -- Look at current character
      c := SUBSTRING ($2 FROM i FOR 1);
      -- If it's a '%' and not the last character then process it as a placeholder
      IF c = '%' AND i != pg_catalog.length($2) THEN
        n := SUBSTRING ($2 FROM (i + 1) FOR 1);
        SELECT INTO res CASE
          WHEN n = 'a' THEN '0'
          WHEN n = 'b' THEN '0'
          WHEN n = 'c' THEN '0'
          WHEN n = 'D' THEN '0'
          WHEN n = 'd' THEN '0'
          WHEN n = 'e' THEN '0'
          WHEN n = 'f' THEN pg_catalog.to_char($1, 'US') 
          WHEN n = 'H' THEN pg_catalog.to_char($1, 'HH24') 
          WHEN n = 'h' THEN pg_catalog.lpad(pg_catalog.to_char($1, 'HH12')::integer % 12, 2, '0')
          WHEN n = 'I' THEN pg_catalog.lpad(pg_catalog.to_char($1, 'HH12')::integer % 12, 2, '0')
          WHEN n = 'i' THEN pg_catalog.to_char($1, 'MI') 
          WHEN n = 'j' THEN '0'
          WHEN n = 'k' THEN pg_catalog.to_char($1, 'FMHH24') 
          WHEN n = 'l' THEN (pg_catalog.to_char($1, 'FMHH12')::integer % 12)::text
          WHEN n = 'M' THEN '0'
          WHEN n = 'm' THEN '0'
          WHEN n = 'p' THEN pg_catalog.to_char($1, 'AM') 
          WHEN n = 'r' THEN pg_catalog.lpad(pg_catalog.to_char($1, 'HH12')::integer % 12, 2, '0') 
                              operator(pg_catalog.||) 
                              pg_catalog.to_char($1, ':MI:SS ') 
                              operator(pg_catalog.||) 
                              CASE WHEN pg_catalog.to_char($1, 'FMHH24')::integer <= 11 THEN 'AM' ELSE 'PM' END
          WHEN n = 'S' THEN pg_catalog.to_char($1, 'SS')
          WHEN n = 's' THEN pg_catalog.to_char($1, 'SS')
          WHEN n = 'T' THEN pg_catalog.to_char($1, 'HH24:MI:SS') 
          WHEN n = 'U' THEN '0'
          WHEN n = 'u' THEN '0'
          WHEN n = 'V' THEN '0'
          WHEN n = 'v' THEN '0'
          WHEN n = 'W' THEN '0'
          WHEN n = 'w' THEN '0'
          WHEN n = 'X' THEN '0'
          WHEN n = 'x' THEN '0'
          WHEN n = 'Y' THEN '0'
          WHEN n = 'y' THEN '0'
          WHEN n = '%' THEN pg_catalog.to_char($1, '%')
          ELSE NULL
        END;
        temp := temp operator(pg_catalog.||) res;
        i := i + 2;
      ELSE
        -- Otherwise just append the character to the string
        temp = temp operator(pg_catalog.||) c;
        i := i + 1;
      END IF; 
    END LOOP;
    RETURN temp;
  END
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- TIME_TO_SEC()
CREATE OR REPLACE FUNCTION time_to_sec(interval)
RETURNS bigint AS $$
  SELECT (EXTRACT(HOURS FROM $1) * 3600
    + EXTRACT(MINUTES FROM $1) * 60
    + EXTRACT(SECONDS FROM $1))::bigint
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- TO_DAYS()
-- XXX: Haven't done integer variant
CREATE OR REPLACE FUNCTION to_days(date)
RETURNS integer AS $$
  SELECT $1 - '0001-01-01 BC'::date
$$ IMMUTABLE STRICT LANGUAGE SQL;

-- UNIX_TIMESTAMP()
CREATE OR REPLACE FUNCTION unix_timestamp()
RETURNS bigint AS $$
  SELECT EXTRACT(EPOCH FROM LOCALTIMESTAMP)::bigint
$$ VOLATILE LANGUAGE SQL;

-- XXX: This gives wrong answers? Time zones?
CREATE OR REPLACE FUNCTION unix_timestamp(timestamp without time zone)
RETURNS bigint AS $$
  SELECT EXTRACT(EPOCH FROM $1)::bigint
$$ VOLATILE LANGUAGE SQL;

-- UTC_DATE()
CREATE OR REPLACE FUNCTION utc_date()
RETURNS date AS $$
  SELECT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')::date
$$ VOLATILE LANGUAGE SQL;

-- UTC_TIME()
CREATE OR REPLACE FUNCTION utc_time()
RETURNS time(0) AS $$
  SELECT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')::time(0)
$$ VOLATILE LANGUAGE SQL;

-- UTC_TIMESTAMP()
CREATE OR REPLACE FUNCTION utc_timestamp()
RETURNS timestamp(0) AS $$
  SELECT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')::timestamp(0)
$$ VOLATILE LANGUAGE SQL;

-- WEEK()
CREATE OR REPLACE FUNCTION _week_mode(mode integer)
RETURNS integer AS $$
  DECLARE
    _WEEK_MONDAY_FIRST  CONSTANT integer := 1;
    _WEEK_FIRST_WEEKDAY CONSTANT integer := 4;
    week_format integer := mode & 7;
  BEGIN
    IF (week_format & _WEEK_MONDAY_FIRST) = 0 THEN
      week_format := week_format # _WEEK_FIRST_WEEKDAY;
    END IF;

    RETURN week_format;
  END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

CREATE OR REPLACE FUNCTION _calc_weekday(qdate date, sundayfirst boolean)
RETURNS integer AS $$
  BEGIN
    RETURN (EXTRACT(DOW FROM qdate)::integer + CASE WHEN sundayfirst THEN 0 ELSE 6 END) % 7;
  END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

CREATE OR REPLACE FUNCTION _calc_days_in_year(year integer)
RETURNS integer AS $$
  BEGIN
    IF (year & 3) = 0 AND ((year % 100) <> 0 OR (year % 400) = 0 AND year <> 0) THEN
      RETURN 366;
    ELSE
      RETURN 365;
    END IF;
  END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

CREATE OR REPLACE FUNCTION _calc_week(qdate anyelement, behavior integer)
RETURNS integer[] AS $$
  DECLARE
    _WEEK_MONDAY_FIRST  CONSTANT integer := 1;
    _WEEK_YEAR          CONSTANT integer := 2;
    _WEEK_FIRST_WEEKDAY CONSTANT integer := 4;
    qyear         integer := EXTRACT(YEAR FROM qdate);
    qmonth        integer := EXTRACT(MONTH FROM qdate);
    qday          integer := EXTRACT(DAY FROM qdate);
    daynr         integer := EXTRACT(DOY FROM qdate);
    yday1         date    := pg_catalog.date_trunc('year', qdate);
    first_daynr   integer := 1;
    monday_first  boolean := (behavior & _WEEK_MONDAY_FIRST) <> 0;
    week_year     boolean := (behavior & _WEEK_YEAR) <> 0;
    first_weekday boolean := (behavior & _WEEK_FIRST_WEEKDAY) <> 0;
    weekday       integer := _calc_weekday(yday1, NOT monday_first);
    days          integer;
  BEGIN
    IF qmonth = 1 AND qday <= 7 - weekday THEN
      IF (NOT week_year) AND ((first_weekday AND weekday <> 0) OR (NOT first_weekday AND weekday >= 4)) THEN
        RETURN array[0, qyear];
      END IF;

      week_year := true;
      qyear := qyear - 1;
      days := _calc_days_in_year(qyear);
      first_daynr := first_daynr - days;
      weekday := (weekday + 53 * 7 - days) % 7;
    END IF;

    IF (first_weekday AND weekday <> 0) OR (NOT first_weekday AND weekday >= 4) THEN
      days := daynr - (first_daynr + (7 - weekday));
    ELSE
      days := daynr - (first_daynr - weekday);
    END IF;

    IF week_year AND days >= 52 * 7 THEN
      weekday := (weekday + _calc_days_in_year(qyear)) % 7;
      IF (NOT first_weekday AND weekday < 4) OR (first_weekday AND weekday = 0) THEN
	qyear := qyear + 1;
        RETURN array[1, qyear];
      END IF;
    END IF;

    RETURN array[days / 7 + 1, qyear];
  END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- YEARWEEK()
CREATE OR REPLACE FUNCTION yearweek(qdate date, mode integer)
RETURNS integer AS $$
  DECLARE
    _WEEK_YEAR CONSTANT integer := 2;
    a integer[] := _calc_week(qdate, _week_mode(mode | _WEEK_YEAR));
    week integer := a[1];
    year integer := a[2];
  BEGIN
    RETURN week + year * 100;
  END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

CREATE OR REPLACE FUNCTION yearweek(date)
RETURNS integer AS $$
  SELECT yearweek($1, 0);
$$ IMMUTABLE STRICT LANGUAGE SQL;

--
-- is_datetime: used to determine if value passed to polymorphic 
-- function is a valid date/time value
--

CREATE OR REPLACE FUNCTION is_datetime ( anyelement )
RETURNS BOOLEAN AS $$
DECLARE d date;
        t TIMESTAMP;
        tz TIMESTAMPTZ;
BEGIN
   d = $1::DATE;
   RETURN TRUE;
EXCEPTION WHEN others THEN
        BEGIN
           t = $1::TIMESTAMP;
                RETURN TRUE;
        EXCEPTION WHEN others THEN
                BEGIN
                        tz = $1::TIMESTAMPTZ;
                        RETURN TRUE;
                EXCEPTION WHEN others THEN
                   RETURN FALSE;
                END;
        END;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

--
-- Polymorphic functions to allow one function 
-- to handle date / timestamp / timestamptz types
--

-- WEEKDAY()
CREATE OR REPLACE FUNCTION weekday( anyelement )
RETURNS integer AS $$
BEGIN
        IF is_datetime ( $1 ) THEN
                CASE WHEN EXTRACT(DOW FROM $1)::integer = 0 THEN
                        RETURN 6;
                     ELSE
                        RETURN EXTRACT(DOW FROM $1)::integer - 1;
                END CASE;
        END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- YEAR()
CREATE OR REPLACE FUNCTION year( anyelement )
RETURNS integer AS $$
BEGIN
        IF is_datetime ( $1 ) THEN
                RETURN EXTRACT(YEAR FROM $1)::integer;
        END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- MONTH()
CREATE OR REPLACE FUNCTION month( anyelement )
RETURNS integer AS $$
BEGIN
	IF is_datetime ( $1 ) THEN
		RETURN EXTRACT(MONTH FROM DATE($1))::integer;
	END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- DAYOFMONTH()
CREATE OR REPLACE FUNCTION dayofmonth( anyelement )
RETURNS integer AS $$
BEGIN
	IF is_datetime ( $1 ) THEN
		RETURN EXTRACT(DAY FROM DATE($1))::integer;
	END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- DAYOFWEEK()
CREATE OR REPLACE FUNCTION dayofweek( anyelement )
RETURNS integer AS $$
BEGIN
	IF is_datetime ( $1 ) THEN
		RETURN EXTRACT(DOW FROM DATE($1))::integer + 1;
	END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- DAYOFYEAR()
CREATE OR REPLACE FUNCTION dayofyear( anyelement )
RETURNS integer AS $$
BEGIN
	IF is_datetime ( $1 ) THEN
		RETURN EXTRACT(DOY FROM DATE($1))::integer;
	END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- HOUR()
CREATE OR REPLACE FUNCTION hour( anyelement )
RETURNS integer AS $$
BEGIN
	IF is_datetime ( $1 ) THEN
		RETURN EXTRACT (HOUR FROM $1)::integer;
	END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- MICROSECOND()
CREATE OR REPLACE FUNCTION microsecond( anyelement )
RETURNS integer AS $$
BEGIN
	IF is_datetime ( $1 ) THEN
		RETURN (EXTRACT(MICROSECONDS FROM $1))::integer % 1000000;
	END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- MINUTE()
CREATE OR REPLACE FUNCTION minute( anyelement )
RETURNS integer AS $$
BEGIN
	IF is_datetime ( $1 ) THEN
		RETURN EXTRACT(MINUTES FROM $1)::integer;
	END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- QUARTER()
CREATE OR REPLACE FUNCTION quarter( anyelement )
RETURNS integer AS $$
BEGIN
	IF is_datetime ( $1 ) THEN
		RETURN EXTRACT(QUARTER FROM DATE($1))::integer;
	END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- SECOND()
CREATE OR REPLACE FUNCTION second( anyelement )
RETURNS integer AS $$
BEGIN
	IF is_datetime ( $1 ) THEN
		RETURN EXTRACT(SECONDS FROM $1)::integer;
	END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

CREATE OR REPLACE FUNCTION week( anyelement, integer )
RETURNS integer AS $$
BEGIN
	IF is_datetime ( $1 ) THEN
		RETURN (_calc_week($1, _week_mode($2)))[1];
	END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

CREATE OR REPLACE FUNCTION week( anyelement )
RETURNS integer AS $$
BEGIN
	IF is_datetime ( $1 ) THEN
		RETURN week($1, 0);
	END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;

-- WEEKOFYEAR()
CREATE OR REPLACE FUNCTION weekofyear( anyelement )
RETURNS integer AS $$
BEGIN
	IF is_datetime ( $1 ) THEN
		RETURN week($1, 3);
	END IF;
	RAISE EXCEPTION 'Invalid date / time value --> %', $1;
END;
$$ IMMUTABLE STRICT LANGUAGE PLPGSQL;
