stdlib/date.rb

Summary

Maintainability
C
1 day
Test Coverage
# backtick_javascript: true

require 'forwardable'
require 'date/infinity'
require 'time'

class Date
  include Comparable
  extend Forwardable

  JULIAN          = Infinity.new
  GREGORIAN       = -Infinity.new
  ITALY           = 2_299_161 # 1582-10-15
  ENGLAND         = 2_361_222 # 1752-09-14
  MONTHNAMES      = [nil] + %w[January February March April May June July August September October November December]
  ABBR_MONTHNAMES = %w[jan feb mar apr may jun jul aug sep oct nov dec]
  DAYNAMES        = %w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday]
  ABBR_DAYNAMES   = %w[Sun Mon Tue Wed Thu Fri Sat]

  class << self
    def wrap(native)
      instance = allocate
      `#{instance}.start = #{ITALY}`
      `#{instance}.date = #{native}`
      instance
    end

    def parse(string, comp = true)
      %x{
        var current_date = new Date();

        var current_day = current_date.getDate(),
            current_month = current_date.getMonth(),
            current_year = current_date.getFullYear(),
            current_wday = current_date.getDay(),
            full_month_name_regexp = #{MONTHNAMES.compact.join('|')};

        function match1(match) { return match[1]; }
        function match2(match) { return match[2]; }
        function match3(match) { return match[3]; }
        function match4(match) { return match[4]; }

        // Converts passed short year (0..99)
        // to a 4-digits year in the range (1969..2068)
        function fromShortYear(fn) {
          return function(match) {
            var short_year = fn(match);

            if (short_year >= 69) {
              short_year += 1900;
            } else {
              short_year += 2000;
            }
            return short_year;
          }
        }

        // Converts month abbr (nov) to a month number
        function fromMonthAbbr(fn) {
          return function(match) {
            var abbr = fn(match).toLowerCase();
            return #{ABBR_MONTHNAMES}.indexOf(abbr) + 1;
          }
        }

        function toInt(fn) {
          return function(match) {
            var value = fn(match);
            return parseInt(value, 10);
          }
        }

        // Depending on the 'comp' value appends 20xx to a passed year
        function to2000(fn) {
          return function(match) {
            var value = fn(match);
            if (comp) {
              return value + 2000;
            } else {
              return value;
            }
          }
        }

        // Converts passed week day name to a day number
        function fromDayName(fn) {
          return function(match) {
            var dayname = fn(match),
                wday = #{DAYNAMES.map(&:downcase)}.indexOf(#{`dayname`.downcase});

            return current_day - current_wday + wday;
          }
        }

        // Converts passed month name to a month number
        function fromFullMonthName(fn) {
          return function(match) {
            var month_name = fn(match);
            return #{MONTHNAMES.compact.map(&:downcase)}.indexOf(#{`month_name`.downcase}) + 1;
          }
        }

        var rules = [
          {
            // DD as month day number
            regexp: /^(\d{2})$/,
            year: current_year,
            month: current_month,
            day: toInt(match1)
          },
          {
            // DDD as year day number
            regexp: /^(\d{3})$/,
            year: current_year,
            month: 0,
            day: toInt(match1)
          },
          {
            // MMDD as month and day
            regexp: /^(\d{2})(\d{2})$/,
            year: current_year,
            month: toInt(match1),
            day: toInt(match2)
          },
          {
            // YYDDD as year and day number in 1969--2068
            regexp: /^(\d{2})(\d{3})$/,
            year: fromShortYear(toInt(match1)),
            month: 0,
            day: toInt(match2)
          },
          {
            // YYMMDD as year, month and day in 1969--2068
            regexp: /^(\d{2})(\d{2})(\d{2})$/,
            year: fromShortYear(toInt(match1)),
            month: toInt(match2),
            day: toInt(match3)
          },
          {
            // YYYYDDD as year and day number
            regexp: /^(\d{4})(\d{3})$/,
            year: toInt(match1),
            month: 0,
            day: toInt(match2)
          },
          {
            // YYYYMMDD as year, month and day number
            regexp: /^(\d{4})(\d{2})(\d{2})$/,
            year: toInt(match1),
            month: toInt(match2),
            day: toInt(match3)
          },
          {
            // mmm YYYY
            regexp: /^([a-z]{3})[\s\.\/\-](\d{3,4})$/,
            year: toInt(match2),
            month: fromMonthAbbr(match1),
            day: 1
          },
          {
            // DD mmm YYYY
            regexp: /^(\d{1,2})[\s\.\/\-]([a-z]{3})[\s\.\/\-](\d{3,4})$/i,
            year: toInt(match3),
            month: fromMonthAbbr(match2),
            day: toInt(match1)
          },
          {
            // mmm DD YYYY
            regexp: /^([a-z]{3})[\s\.\/\-](\d{1,2})[\s\.\/\-](\d{3,4})$/i,
            year: toInt(match3),
            month: fromMonthAbbr(match1),
            day: toInt(match2)
          },
          {
            // YYYY mmm DD
            regexp: /^(\d{3,4})[\s\.\/\-]([a-z]{3})[\s\.\/\-](\d{1,2})$/i,
            year: toInt(match1),
            month: fromMonthAbbr(match2),
            day: toInt(match3)
          },
          {
            // YYYY-MM-DD YYYY/MM/DD YYYY.MM.DD
            regexp: /^(\-?\d{3,4})[\s\.\/\-](\d{1,2})[\s\.\/\-](\d{1,2})$/,
            year: toInt(match1),
            month: toInt(match2),
            day: toInt(match3)
          },
          {
            // YY-MM-DD
            regexp: /^(\d{2})[\s\.\/\-](\d{1,2})[\s\.\/\-](\d{1,2})$/,
            year: to2000(toInt(match1)),
            month: toInt(match2),
            day: toInt(match3)
          },
          {
            // DD-MM-YYYY
            regexp: /^(\d{1,2})[\s\.\/\-](\d{1,2})[\s\.\/\-](\-?\d{3,4})$/,
            year: toInt(match3),
            month: toInt(match2),
            day: toInt(match1)
          },
          {
            // ddd
            regexp: new RegExp("^(" + #{DAYNAMES.join('|')} + ")$", 'i'),
            year: current_year,
            month: current_month,
            day: fromDayName(match1)
          },
          {
            // monthname daynumber YYYY
            regexp: new RegExp("^(" + full_month_name_regexp + ")[\\s\\.\\/\\-](\\d{1,2})(th|nd|rd)[\\s\\.\\/\\-](\\-?\\d{3,4})$", "i"),
            year: toInt(match4),
            month: fromFullMonthName(match1),
            day: toInt(match2)
          },
          {
            // monthname daynumber
            regexp: new RegExp("^(" + full_month_name_regexp + ")[\\s\\.\\/\\-](\\d{1,2})(th|nd|rd)", "i"),
            year: current_year,
            month: fromFullMonthName(match1),
            day: toInt(match2)
          },
          {
            // daynumber monthname YYYY
            regexp: new RegExp("^(\\d{1,2})(th|nd|rd)[\\s\\.\\/\\-](" + full_month_name_regexp + ")[\\s\\.\\/\\-](\\-?\\d{3,4})$", "i"),
            year: toInt(match4),
            month: fromFullMonthName(match3),
            day: toInt(match1)
          },
          {
            // YYYY monthname daynumber
            regexp: new RegExp("^(\\-?\\d{3,4})[\\s\\.\\/\\-](" + full_month_name_regexp + ")[\\s\\.\\/\\-](\\d{1,2})(th|nd|rd)$", "i"),
            year: toInt(match1),
            month: fromFullMonthName(match2),
            day: toInt(match3)
          }
        ]

        var rule, i, match;

        for (i = 0; i < rules.length; i++) {
          rule = rules[i];
          match = rule.regexp.exec(string);
          if (match) {
            var year = rule.year;
            if (typeof(year) === 'function') {
              year = year(match);
            }

            var month = rule.month;
            if (typeof(month) === 'function') {
              month = month(match) - 1
            }

            var day = rule.day;
            if (typeof(day) === 'function') {
              day = day(match);
            }

            var result = new Date(year, month, day);

            // an edge case, JS can't handle 'new Date(1)', minimal year is 1970
            if (year >= 0 && year <= 1970) {
              result.setFullYear(year);
            }

            return #{wrap `result`};
          }
        }
      }
      raise ArgumentError, 'invalid date'
    end

    def today
      wrap `new Date()`
    end

    def gregorian_leap?(year)
      `(new Date(#{year}, 1, 29).getMonth()-1) === 0`
    end

    alias civil new
  end

  def initialize(year = -4712, month = 1, day = 1, start = ITALY)
    %x{
      // Because of Gregorian reform calendar goes from 1582-10-04 to 1582-10-15.
      // All days in between end up as 4 october.
      if (year === 1582 && month === 10 && day > 4 && day < 15) {
        day = 4;
      }
    }

    @date = `new Date(year, month - 1, day)`
    @start = start
  end

  attr_reader :start

  def <=>(other)
    %x{
      if (other.$$is_number) {
        return #{jd <=> other}
      }

      if (#{::Date === other}) {
        var a = #{@date}, b = #{other}.date;
        if (!Opal.is_a(#{self}, #{::DateTime})) a.setHours(0, 0, 0, 0);
        if (!Opal.is_a(#{other}, #{::DateTime})) b.setHours(0, 0, 0, 0);

        if (a < b) {
          return -1;
        }
        else if (a > b) {
          return 1;
        }
        else {
          return 0;
        }
      } else {
        return nil;
      }
    }
  end

  def >>(n)
    `if (!n.$$is_number) #{raise ::TypeError}`

    self << -n
  end

  def <<(n)
    `if (!n.$$is_number) #{raise ::TypeError}`

    prev_month(n)
  end

  def clone
    date = Date.wrap(@date.dup)
    `date.start = #{@start}`
    date
  end

  def_delegators :@date, :sunday?, :monday?, :tuesday?, :wednesday?, :thursday?, :friday?, :saturday?,
    :day, :month, :year, :wday, :yday

  alias mday day
  alias mon month

  def jd
    %x{
    //Adapted from http://www.physics.sfasu.edu/astro/javascript/julianday.html

    var mm = #{@date}.getMonth() + 1,
        dd = #{@date}.getDate(),
        yy = #{@date}.getFullYear(),
        hr = 12, mn = 0, sc = 0,
        ggg, s, a, j1, jd;

    hr = hr + (mn / 60) + (sc/3600);

    ggg = 1;
    if (yy <= 1585) {
      ggg = 0;
    }

    jd = -1 * Math.floor(7 * (Math.floor((mm + 9) / 12) + yy) / 4);

    s = 1;
    if ((mm - 9) < 0) {
      s =- 1;
    }

    a = Math.abs(mm - 9);
    j1 = Math.floor(yy + s * Math.floor(a / 7));
    j1 = -1 * Math.floor((Math.floor(j1 / 100) + 1) * 3 / 4);

    jd = jd + Math.floor(275 * mm / 9) + dd + (ggg * j1);
    jd = jd + 1721027 + 2 * ggg + 367 * yy - 0.5;
    jd = jd + (hr / 24);

    return jd;
    }
  end

  def julian?
    `#{@date} < new Date(1582, 10 - 1, 15, 12)`
  end

  def new_start(start)
    new_date = clone
    `new_date.start = start`
    new_date
  end

  def next
    self + 1
  end

  def -(date)
    %x{
      if (date.date) {
        return Math.round((#{@date} - #{date}.date) / (1000 * 60 * 60 * 24));
      }
    }
    prev_day(date)
  end

  def +(date)
    next_day(date)
  end

  def prev_day(n = 1)
    %x{
      if (n.$$is_number) {
        var result = #{clone};
        result.date.setDate(#{@date}.getDate() - n);
        return result;
      }
      else {
        #{raise ::TypeError};
      }
    }
  end

  def next_day(n = 1)
    `if (!n.$$is_number) #{raise ::TypeError}`
    prev_day(-n)
  end

  def prev_month(n = 1)
    %x{
      if (!n.$$is_number) #{raise ::TypeError}
      var result = #{clone}, date = result.date, cur = date.getDate();
      date.setDate(1);
      date.setMonth(date.getMonth() - n);
      date.setDate(Math.min(cur, #{Date._days_in_month(`date.getFullYear()`, `date.getMonth()`)}));
      return result;
    }
  end

  def next_month(n = 1)
    `if (!n.$$is_number) #{raise ::TypeError}`
    prev_month(-n)
  end

  def prev_year(years = 1)
    `if (!years.$$is_number) #{raise ::TypeError}`
    self.class.new(year - years, month, day)
  end

  def next_year(years = 1)
    `if (!years.$$is_number) #{raise ::TypeError}`
    prev_year(-years)
  end

  def strftime(format = '')
    %x{
      if (format == '') {
        return #{to_s};
      }

      return #{@date.strftime(format)}
    }
  end

  def to_s
    %x{
      var d = #{@date}, year = d.getFullYear(), month = d.getMonth() + 1, day = d.getDate();
      if (month < 10) { month = '0' + month; }
      if (day < 10) { day = '0' + day; }
      return year + '-' + month + '-' + day;
    }
  end

  def to_time
    Time.new(year, month, day)
  end

  def to_date
    self
  end

  def to_datetime
    DateTime.new(year, month, day)
  end

  def to_n
    @date
  end

  def step(limit, step = 1, &block)
    steps_count = (limit - self).to_i

    steps = if steps_count * step < 0
              []
            elsif steps_count < 0
              (0..-steps_count).step(step.abs).map(&:-@).reverse
            else
              (0..steps_count).step(step.abs)
            end

    result = steps.map { |i| self + i }


    if block_given?
      result.each { |i| yield(i) }
      self
    else
      result
    end
  end

  def upto(max, &block)
    step(max, 1, &block)
  end

  def downto(min, &block)
    step(min, -1, &block)
  end

  def cwday
    `#{@date}.getDay() || 7`
  end

  def cweek
    %x{
      var d = new Date(#{@date});
      d.setHours(0,0,0);
      d.setDate(d.getDate()+4-(d.getDay()||7));
      return Math.ceil((((d-new Date(d.getFullYear(),0,1))/8.64e7)+1)/7);
    }
  end

  def self._days_in_month(year, month)
    %x{
      var leap = ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
      return [31, (leap ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
    }
  end

  alias eql? ==
  alias succ next
end

require 'date/date_time'
require 'date/formatters'