Category:Jq/Date.jq

From Rosetta Code
module {
  "name": "Date.jq",
  "description": "Dates and times since the beginning of Year 1 in the Gregorian Calendar",
  "version": "0.0.1",
  "homepage": "https://rosettacode.org/w/index.php?title=Category:Jq/Date.jq",
  "license": "MIT",
  "author": "pkoppstein at gmail dot com",
};

# This module provides a Date constructor and some associated date and time functions
# that assume a Gregorian calendar with dates on or after January 1 in the year 1.
# 
# The calendrical computations are based on isLeapYear/0; no corrections for leap seconds are made.

# Examples:
# To compute the number of days between two Dates, use daysBeforeDate/0.
# To compute the Date corresponding to a given number of days before or after another Date, use addDays/1.

## Motivation:
# Currently jq's support for some functions related to dates is limited, e.g.

# jq -n '[1792,8,22,0,0,0,6,265] | mktime'

# results in an error message complaining about "invalid gmtime representation".
# Also, jaq does not currently provide any specific time or date built-ins.

## Date and the Epoch

# The constructor Date($year; $month; $day) simply yields the JSON
# object {"year": $year, "month": $month, "day": $day}, but
# the date functions defined here assume the Gregorian
# calendar and that $year, $month, and $day are all positive integers.

# For present purposes the epoch begins at the beginning of New Year's
# Day of the year 1, that is, on the date represented by Date(1;1;1).

def Date($year; $month; $day): {$year, $month, $day};

def monthNames:
  ["", "January", "February", "March", "April", "May", "June", 
   "July", "August",  "September", "October", "November", "December"];

# Input: an integer year
def isLeapYear:
  .%4 == 0 and (.%100 != 0 or .%400 == 0);

def daysInYear: if isLeapYear then 366 else 365 end;

def daysBeforeMonthArray:
  if isLeapYear
  then  [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]
  else  [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]
  end;

# Input: a non-negative integer representing a year
# Output: the number of days from the eve of Date(1;1;1) to the start of the given year,
# with the understanding that:
# 1 | daysBeforeYear #=> 0
# 2 | daysBeforeYear #=> 365
def daysBeforeYear:
  if . <= 0 or . != trunc then "daysBeforeYear expects the specified year to be non-negative (vs \(.))" | error
  else (.-1) as $y
  | $y*365 + (($y/4)|floor) - (($y/100)|floor) + (($y/400)|floor)
  end;

# Input: a possibly augmented Date
def daysBeforeDate:
  (.year|daysBeforeYear)
  + (.year|daysBeforeMonthArray)[.month-1]
  + .day - 1;

# Input: a Date possibly augmented with any combination of {hour, minute, second}
# Output: The number of seconds from the beginning of the Epoch (Date(1;1;1)) to
# the beginning of the given date or date and time, assuming the latter is later than the former.
# Note that .year, .month, and .day must be non-negative, but
# .hour, .minute, and .second need only be numeric.
def seconds:
  if .year <= 0 or .month <= 0 or .day <= 0 then "improper Date (\(.))"|error end
  | (.year | daysBeforeYear) as $d1 # days before start of year
  | (.year|daysBeforeMonthArray[.month-1]) as $d2
  | (($d1 + $d2 + .day - 1) * 86400)
  + (if .hour   then (.hour-1) * 3600 else 0 end)
  + (if .minute then (.minute-1) * 60 else 0 end)
  + (if .second then (.second-1)      else 0 end) ;

# Input: a positive integer (a number of days)
# Output: Date corresponding to the given number of days after the day before Day(1;1;1)
# Example: 1 | today #=> Day(1;1;1)
def toDate:
  if . < 0 or . != trunc then "toDate requires that . be a non-negative integer"|error end
  | . as $in
  | { year: (1 + ((./366) | floor)) }
  | .days = (.year | daysBeforeYear)
  | .excess = ($in - .days)
  | until(.excess <= (.year|daysInYear);
      .year += 1
      | .days = (.year | daysBeforeYear)
      | .excess = ($in - .days) )
  | if .excess <= 31
    then Date(.y; 1; .excess) 
    else (.year|daysBeforeMonthArray) as $calendar
      | .month = 0
      | until( .excess <= $calendar[.month];
          .month += 1 )
      | .day = .excess - $calendar[.month - 1]
      | {year, month, day}
    end;

# Input: Date
# Output: the Date that is $days later, ignoring leap seconds.
# $days should be an integer, but if negative, it must satisfy the requirement:
# daysBeforeDate + $days >= 0
def addDays($days):
  daysBeforeDate + 1 + $days | toDate;

This category currently contains no pages or media.