lib/parsi-date.rb in parsi-date-0.1 vs lib/parsi-date.rb in parsi-date-0.2

- old
+ new

@@ -1,69 +1,354 @@ # encoding: utf-8 +# +# parsi-date.rb +# +# Author: Hassan Zamani 2012 +# +# == Overview +# +# Date class represent a date in persian (jalali) calendar +# +# === Ways of calculating the date. +# +# In common usage, the date is reckoned in years since or before the Common Era +# (Hegira of Muhammad from Mecca to Medina in 622 CE), then as a month and +# day-of-the-month within the current year. This is known as the *Civil* *Date*, +# and abbreviated as +civil+ in the Date class. +# +# Instead of year, month-of-the-year, and day-of-the-month, the date can also +# be reckoned in terms of year and day-of-the-year. This is known as the *Ordinal* +# *Date*, and is abbreviated as +ordinal+ in the Date class. (Note that referring +# to this as the Julian date is incorrect.) +# +# For scientific purposes, it is convenient to refer to a date simply as a day +# count, counting from an arbitrary initial day. The date first chosen for this +# was January 1, 4713 BCE (-5335/9/1 in hijri solar calendar). A count of days from +# this date is the *Julian* *Day* *Number* or *Julian* *Date*, which is abbreviated +# as +jd+ in the Date class. This is in local time, and counts from midnight on the +# initial day. The stricter usage is in UTC, and counts from midday on the initial +# day. This is referred to in the Date class as the *Astronomical* *Julian* *Day* +# *Number*, and abbreviated as +ajd+. In the Date class, the Astronomical Julian +# Day Number includes fractional days. +# +# Another absolute day count is the *Modified* *Julian* *Day* *Number*, which +# takes November 17, 1858 (1237/08/26 in hijri solar calendar) as its initial day. +# This is abbreviated as +mjd+ in the Date class. There is also an *Astronomical* +# *Modified* *Julian* *Day* *Number*, which is in UTC and includes fractional days. +# This is abbreviated as +amjd+ in the Date class. Like the Modified Julian Day +# Number (and unlike the Astronomical Julian Day Number), it counts from midnight. +# +# === Time Zones +# +# DateTime objects support a simple representation of time zones. Time zones are +# represented as an offset from UTC, as a fraction of a day. This offset is the +# how much local time is later (or earlier) than UTC. UTC offset 0 is centred on +# England (also known as GMT). As you travel east, the offset increases until you +# reach the dateline in the middle of the Pacific Ocean; as you travel west, the +# offset decreases. This offset is abbreviated as +offset+ in the DateTime class. +# +# This simple representation of time zones does not take into account the common +# practice of Daylight Savings Time or Summer Time. +# +# Most DateTime methods return the date and the time in local time. The two +# exceptions are #ajd() and #amjd(), which return the date and time in UTC time, +# including fractional days. +# +# The Date class does not support time zone offsets, in that there is no way to +# create a Date object with a time zone. However, methods of the Date class when +# used by a DateTime instance will use the time zone offset of this instance. +# require 'date' module Parsi - MONTHNAMES = [nil] + %w(فروردین اردیبهشت خرداد تیر مرداد شهریور مهر آبان آذر دی بهمن اسفند) - ABBR_MONTHNAMES = [nil] + %w(Far Ord Kho Tir Mor Sha Meh Abn Azr Dey Bah Esf) - DAYNAMES = %w(شنده یک‌شنده دوشنده سه‌شنده چهارشنده چنج‌شنده جمعه) - ABBR_DAYNAMES = %w(ش ۱ش ۲ش ۳ش ۴ش ۵ش ج) - [MONTHNAMES, ABBR_MONTHNAMES, DAYNAMES, ABBR_DAYNAMES].each &:freeze - + # Class representing a date. + # + # See the documentation to the file parsi-date.rb for an overview. + # + # Internally, the date is represented as an Astronomical Julian Day Number, +ajd+. + # (There is also an +offset+ field for a time zone offset, but this is only for + # the use of the DateTime subclass.) + # + # A new Date object is created using one of the object creation class methods + # named after the corresponding date format, and the arguments appropriate to + # that date format; for instance, Date::civil() (aliased to Date::new()) with + # year, month, and day-of-month, or Date::ordinal() with year and day-of-year. + # + # Date objects are immutable once created. + # + # Once a Date has been created, date values can be retrieved for the different + # date formats supported using instance methods. For instance, #mon() gives the + # Civil month and #yday() gives the Ordinal day of the year. Date values can be + # retrieved in any format, regardless of what format was used to create the + # Date instance. + # + # The Date class includes the Comparable module, allowing + # date objects to be compared and sorted, ranges of dates + # to be created, and so forth. class Date + include Comparable - attr_reader :year, :month, :day + # Full month names, in Farsi. Months count from 1 to 12; a + # month's numerical representation indexed into this array + # gives the name of that month (hence the first element is nil). + MONTHNAMES = [nil] + %w(فروردین اردیبهشت خرداد تیر مرداد شهریور مهر آبان آذر دی بهمن اسفند) - DAYS_TO_FIRST_OF_MONTH = [nil, 0, 31, 62, 93, 124, 155, 186, 216, 246, 276, 306, 336] - DAYS_IN_MONTH = [nil, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29] - JALALI_EPOCH = 1948320.5 + # Full names of days of the week, in Farsi. Days of the week + # count from 0 to 6 (except in the commercial week); a day's numerical + # representation indexed into this array gives the name of that day. + DAYNAMES = %w(یک‌شنده دوشنده سه‌شنده چهارشنده چنج‌شنده جمعه شنده) - class << self + # Abbreviated month names, in English. + ABBR_MONTHNAMES = [nil] + %w(Far Ord Kho Tir Mor Sha Meh Abn Azr Dey Bah Esf) + # Abbreviated day names, in Farsi. + ABBR_DAYNAMES = %w(۱ش ۲ش ۳ش ۴ش ۵ش ج ش) + + [MONTHNAMES, ABBR_MONTHNAMES, DAYNAMES, ABBR_DAYNAMES].each do |xs| + xs.each{|x| x.freeze unless x.nil?}.freeze + end + + HALF_DAYS_IN_DAY = Rational(1, 2) # :nodoc: + HOURS_IN_DAY = Rational(1, 24) # :nodoc: + MINUTES_IN_DAY = Rational(1, 1440) # :nodoc: + SECONDS_IN_DAY = Rational(1, 86400) # :nodoc: + MILLISECONDS_IN_DAY = Rational(1, 86400*10**3) # :nodoc: + NANOSECONDS_IN_DAY = Rational(1, 86400*10**9) # :nodoc: + MILLISECONDS_IN_SECOND = Rational(1, 10**3) # :nodoc: + NANOSECONDS_IN_SECOND = Rational(1, 10**9) # :nodoc: + + JALALI_EPOCH_IN_AJD = Rational(3896641, 2) # :nodoc: + MJD_EPOCH_IN_AJD = Rational(4800001, 2) # 1858-11-17 # :nodoc: + UNIX_EPOCH_IN_AJD = Rational(4881175, 2) # 1970-01-01 # :nodoc: + JALALI_EPOCH_IN_CJD = 1948321 # :nodoc: + MJD_EPOCH_IN_CJD = 2400001 # :nodoc: + UNIX_EPOCH_IN_CJD = 2440588 # :nodoc: + LD_EPOCH_IN_CJD = 2299160 # :nodoc: + DAYS_IN_MONTH = [nil, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29] # :nodoc: + + shared_methods = Module.new do + + # Returns true if the given year is a leap year of the calendar. def leap? year ((((((year - ((year > 0) ? 474 : 473)) % 2820) + 474) + 38) * 682) % 2816) < 682 end - alias :exist? :leap? + alias_method :jalali_leap?, :leap? - def valid? year, month, day - return false unless year.is_a?(Fixnum) && month.is_a?(Fixnum) && day.is_a?(Fixnum) - return true if leap?(year) && month == 12 && day == 30 + private - 1 <= month && month <= 12 && 1 <= day && day <= DAYS_IN_MONTH[month] - end + DAYS_TO_FIRST_OF_MONTH = [nil, 0, 31, 62, 93, 124, 155, 186, 216, 246, 276, 306, 336] # :nodoc: - def jd jday=0 - jday = jday.floor + # Convert a Civil Date to a Julian Day Number and returns the corresponding Julian Day Number. + def civil_to_jd year, month, day # :nodoc: + epbase = year - 474 + epyear = 474 + (epbase % 2820) - depoch = (jday - Date.new(475, 1, 1).jd).floor - cycle = depoch / 1029983 - cyear = depoch % 1029983 + day + DAYS_TO_FIRST_OF_MONTH[month] + + (epyear * 682 - 110) / 2816 + + (epyear - 1) * 365 + + (epbase / 2820 * 1029983) + + (JALALI_EPOCH_IN_CJD - 1) + end + # Convert a Julian Day Number to a Civil Date. +jday+ is the Julian Day Number. + # + # Returns the corresponding [year, month, day_of_month] as a three-element array. + def jd_to_civil jday + depoch = (jday - first_day_of_year(475)) + cycle, cyear = depoch.divmod 1029983 + if cyear == 1029982 - ycycle = 2820 + ycycle = 2820 else - aux1 = cyear / 366 - aux2 = cyear % 366 - ycycle = (2134 * aux1 + 2816 * aux2 + 2815) / 1028522 + aux1 + 1 + aux1, aux2 = cyear.divmod 366 + ycycle = (2134 * aux1 + 2816 * aux2 + 2815) / 1028522 + aux1 + 1 end - year = ycycle + 2820 * cycle + 474 - year -= 1 if year <= 0 + year = ycycle + 2820 * cycle + 474 + yday = jday - first_day_of_year(year) + 1 + month = ((yday <= 186) ? yday / 31.0 : (yday - 6) / 30.0).ceil + day = (jday - first_day_of_month(year, month) + 1) + [year, month, day] + end - yday = jday - Date.new(year, 1, 1).jd + 1 - month = (yday <= 186) ? (yday / 31.0).ceil : ((yday - 6) / 30.0).ceil - day = (jday - Date.new(year, month, 1).jd + 1).floor - Date.new year, month, day + # Do +year+, +month+, and day-of-month +day+ make a valid Civil Date? + # Returns the corresponding Julian Day Number if they do, nil if they don't. + # Invalid values cause an ArgumentError to be raised. + def _valid_civil? year, month, day # :nodoc: + return unless year.is_a?(Fixnum) && month.is_a?(Fixnum) && day.is_a?(Fixnum) + return civil_to_jd(year, 12, 30) if leap?(year) && month == 12 && day == 30 + + if 1 <= month && month <= 12 && 1 <= day && day <= DAYS_IN_MONTH[month] + return civil_to_jd year, month, day + else + nil + end end - alias :civil :new + # Do the +year+ and day-of-year +yday+ make a valid Ordinal Date? + # Returns the corresponding Julian Day Number if they do, or nil if they don't. + def _valid_ordinal? year, yday + ordinal_to_jd year, yday + end - def ordinal year, ydays=1 - jd = Date.new(year, 1, 1).jd + ydays - 1 - Date.jd jd + def first_day_of_year year # :nodoc: + civil_to_jd year, 1, 1 end + def last_day_of_year year # :nodoc: + _valid_civil?(year, 12, 30) || civil_to_jd(year, 12, 29) + end + + def first_day_of_month year, month # :nodoc: + civil_to_jd year, month, 1 + end + + def last_day_of_month year, month # :nodoc: + _valid_civil?(year, month, 31) || _valid_civil?(year, month, 30) || _valid_civil?(year, month, 29) + end + + # Convert an Ordinal Date to a Julian Day Number. + # + # +year+ and +yday+ are the year and day-of-year to convert. + # + # Returns the corresponding Julian Day Number. + def ordinal_to_jd year, yday # :nodoc: + first_day_of_year(year) + yday - 1 + end + + # Convert a Julian Day Number to an Ordinal Date. + # + # +jday+ is the Julian Day Number to convert. + # + # Returns the corresponding Ordinal Date as [year, day_of_year] + def jd_to_ordinal jday # :nodoc: + year = jd_to_civil(jd).first + yday = jday - first_day_of_year(year) + 1 + [year, yday] + end + + # Convert an Astronomical Julian Day Number to a (civil) Julian Day Number. + # + # +ajd+ is the Astronomical Julian Day Number to convert. +offset+ is the offset from + # UTC as a fraction of a day (defaults to 0). + # + # Returns the (civil) Julian Day Number as [day_number, fraction] where +fraction+ is + # always 1/2. + def ajd_to_jd ajd, offset=0 # :nodoc: + (ajd + offset + HALF_DAYS_IN_DAY).divmod(1) + end + + # Convert a (civil) Julian Day Number to an Astronomical Julian Day Number. + # + # +jd+ is the Julian Day Number to convert, and +fraction+ is a fraction of day. + # +offset+ is the offset from UTC as a fraction of a day (defaults to 0). + # + # Returns the Astronomical Julian Day Number as a single numeric value. + def jd_to_ajd jd, fraction, offset=0 # :nodoc: + jd + fraction - offset - HALF_DAYS_IN_DAY + end + + # Convert an +hour+, +minute+, +second+s period to a fractional day. + def time_to_day_fraction hour, minute, second # :nodoc: + Rational(hour * 3600 + minute * 60 + second, 86400) + end + + # Convert an Astronomical Modified Julian Day Number to an Astronomical Julian Day Number. + def amjd_to_ajd(amjd) amjd + MJD_EPOCH_IN_AJD end # :nodoc: + + # Convert an Astronomical Julian Day Number to an Astronomical Modified Julian Day Number. + def ajd_to_amjd(ajd) ajd - MJD_EPOCH_IN_AJD end # :nodoc: + + # Convert a Modified Julian Day Number to a Julian Day Number. + def mjd_to_jd(mjd) mjd + MJD_EPOCH_IN_CJD end # :nodoc: + + # Convert a Julian Day Number to a Modified Julian Day Number. + def jd_to_mjd(jd) jd - MJD_EPOCH_IN_CJD end # :nodoc: + + # Convert a count of the number of days since the adoption of the Gregorian Calendar + # (in Italy) to a Julian Day Number. + def ld_to_jd(ld) ld + LD_EPOCH_IN_CJD end # :nodoc: + + # Convert a Julian Day Number to the number of days since the adoption of the + # Gregorian Calendar (in Italy). + def jd_to_ld(jd) jd - LD_EPOCH_IN_CJD end # :nodoc: + + # Convert a Julian Day Number to the day of the week. + # + # Sunday is day-of-week 0; Saturday is day-of-week 6. + def jd_to_wday(jd) (jd + 1) % 7 end # :nodoc: + + # Is +jd+ a valid Julian Day Number? + # + # If it is, returns it. In fact, any value is treated as a valid Julian Day Number. + def _valid_jd?(jd) jd end # :nodoc: + end + + extend shared_methods + include shared_methods + + class << self + + alias_method :new!, :new + + def valid_jd? jd + !!_valid_jd?(jd) + end + + def valid_ordinal? year, yday + !!_valid_ordinal?(year, yday) + end + + def valid_civil? year, month, day + !!_valid_civil?(year, month, day) + end + alias :valid_date? :valid_civil? + alias :valid? :valid_civil? + alias :exist? :valid_civil? + + # Create a new Date object from a Julian Day Number. + # + # +jday+ is the Julian Day Number; if not specified, it defaults to 0. + # + # examples: + # Parsi::Date.jd 2456229 # => #<Parsi::Date: 1391-08-07> + # Parsi::Date.jd 2456230 # => #<Parsi::Date: 1391-08-08> + # Parsi::Date.jd # => #<Parsi::Date: -5335-09-01> + # + def jd jday=0 + jd = _valid_jd? jday + new! jd_to_ajd(jday, 0, 0), 0 + end + + # Create a new Date object from an Ordinal Date, specified by +year+ and day-of-year +yday+. + # +yday+ can be negative, in which it counts backwards from the end of the year. + # + # examples: + # Parsi::Date.ordinal 1390 # => #<Parsi::Date: 1390-01-01> + # Parsi::Date.ordinal 1391, 120 # => #<Parsi::Date: 1391-04-27> + # Parsi::Date.ordinal 1390, -1 # => #<Parsi::Date: 1389-12-29> + # + def ordinal year=0, yday=1 + raise ArgumentError, 'invalid date' unless jd = _valid_ordinal?(year, yday) + new! jd_to_ajd(jd, 0, 0), 0 + end + + # Create a new Date object for the Civil Date specified by +year+, +month+, and + # day-of-month +day+. + def civil year=1, month=1, day=1 + raise ArgumentError, 'invalid date' unless jd = _valid_civil?(year, month, day) + new! jd_to_ajd(jd, 0, 0), 0 + end + alias_method :new, :civil + + # Parses the given representation of date and time, and creates a date object. + # + # If the optional second argument is true and the detected year is in the range “00” to “99”, + # considers the year a 2-digit form and makes it full. + # + # For def parse string, comp=true # TODO: Add more parse options, for example parse '۴ام فروردین ۱۳۹۱' m = string.match /(?<year>\d+)(\/|-| )(?<month>\d+)(\/|-| )(?<day>\d+)/ m ||= string.match /(?<year>\d+)(?<month>\d{2})(?<day>\d{2})/ if m.nil? @@ -72,81 +357,241 @@ year, month, day = m[:year].to_i, m[:month].to_i, m[:day].to_i if comp && m[:year].length == 2 centry = Date.today.year / 100 year = (m[:year].prepend centry.to_s).to_i end - if Date.valid? year, month, day - Date.new year, month, day + if jd = _valid_civil?(year, month, day) + new! jd_to_ajd(jd, 0, 0), 0 else raise ArgumentError.new 'invalid date' end end end + # Create a new Date object representing today. def today - Date.jd ::Date.today.jd + ::Date.today.to_parsi end + end - def tomorrow ; today + 1 end - def yesterday; today - 1 end + # Create a new Date object. + # + # +ajd+ is the Astronomical Julian Day Number. + # +offset+ is the offset from UTC as a fraction of a day. + def initialize ajd=0, offset=0 + @ajd, @offset = ajd, offset end - def initialize year=0, month=1, day=1 - raise ArgumentError.new 'invalid date' unless Date.valid? year, month, day - @year, @month, @day = year, month, day + # Get the date as an Astronomical Julian Day Number. + def ajd + @ajd end - alias :mon :month - alias :mday :day - - def to_s sep='/' - "%d%s%02d%s%02d" % [year, sep, month, sep, day] + def offset + @offset end + private :offset - def inspect - "#<#{self.class}: #{to_s('-')}>" + # Get the date as an Astronomical Modified Julian Day Number. + def amjd + @amjd ||= ajd_to_amjd ajd end + # Get the date as a Julian Day Number. def jd - @jd ||= begin - epbase = year - ((year >= 0) ? 474 : 473) - epyear = 474 + epbase % 2820 + @jd ||= ajd_to_jd(ajd, offset).first + end - day + DAYS_TO_FIRST_OF_MONTH[month] + - (epyear * 682 - 110) / 2816 + - (epyear - 1) * 365 + - (epbase / 2820 * 1029983) + - (JALALI_EPOCH - 1) + 0.5 - end + # Get any fractional day part of the date. + def day_fraction + @day_fraction ||= ajd_to_jd(ajd, offset).last end - def mjd ; (jd - 2400001).floor end - def ld ; (jd - 2299160).floor end - def ajd ; gregorian.ajd end - def amjd ; gregorian.amjd end + # Get the date as a Modified Julian Day Number. + def mjd + @mjd ||= jd_to_mjd jd + end + # Get the date as the number of days since the Day of Calendar + # Reform (in Italy and the Catholic countries). + def ld + @ld ||= jd_to_ld jd + end + + # Get the date as a Civil Date, [year, month, day_of_month] + def civil # :nodoc: + @civil ||= jd_to_civil jd + end + + # Get the date as an Ordinal Date, [year, day_of_year] + def ordinal # :nodoc: + @ordinal ||= jd_to_ordinal jd + end + + private :civil, :ordinal + + # Get the year of this date. + def year() civil[0] end + + # Get the month of this date. + # + # Farvardin is month 1. + def mon() civil[1] end + alias_method :month, :mon + + # Get the day-of-the-month of this date. + def mday() civil[2] end + alias_method :day, :mday + + # Get the day-of-the-year of this date. + # + # January 1 is day-of-the-year 1 + def yday; ordinal[1] end + + # Get the week day of this date. Sunday is day-of-week 0; + # Saturday is day-of-week 6. def wday - (gregorian.wday + 1) % 7 + @wday ||= jd_to_wday jd end - def cwday - wday + 1 + ::Date::DAYNAMES.each_with_index do |n, i| + define_method(n.downcase + '?'){ wday == i } end - def yday - (jd - first_of_year.jd + 1).to_i + # Return a new Date object that is +n+ days later than the current one. + # + # +n+ may be a negative value, in which case the new Date is earlier + # than the current one; however, #-() might be more intuitive. + # + # If +n+ is not a Numeric, a TypeError will be thrown. In particular, + # two Dates cannot be added to each other. + def + n + case n + when Numeric + return self.class.new!(ajd + n, offset) + end + raise TypeError, 'expected numeric' end - def leap? - Date.leap? year + # If +x+ is a Numeric value, create a new Date object that is +x+ days + # earlier than the current one. + # + # If +x+ is a Date, return the number of days between the two dates; or, + # more precisely, how many days later the current date is than +x+. + # + # If +x+ is neither Numeric nor a Date, a TypeError is raised. + def - x + case x + when Numeric + return self.class.new!(ajd - x, offset) + when Date + return ajd - x.ajd + end + raise TypeError, 'expected numeric or date' end + # Compare this date with another date. + # + # +other+ can also be a Numeric value, in which case it is interpreted as an + # Astronomical Julian Day Number. + # + # Comparison is by Astronomical Julian Day Number, including fractional days. + # This means that both the time and the timezone offset are taken into account + # when comparing two DateTime instances. When comparing a DateTime instance + # with a Date instance, the time of the latter will be considered as falling + # on midnight UTC. + def <=> other + case other + when Numeric + return ajd <=> other + when Date + return ajd <=> other.ajd + when ::Date + return ajd <=> other.ajd + else + begin + left, right = other.coerce(self) + return left <=> right + rescue NoMethodError + end + end + nil + end + + # The relationship operator for Date. + # + # Compares dates by Julian Day Number. When comparing two DateTime instances, + # or a DateTime with a Date, the instances will be regarded as equivalent if + # they fall on the same date in local time. + def === (other) + case other + when Numeric + return jd == other + when Date; return jd == other.jd + else + begin + l, r = other.coerce(self) + return l === r + rescue NoMethodError + end + end + false + end + + def next_day(n=1) self + n end + def prev_day(n=1) self - n end + + # Return a new Date one day after this one. + def next() next_day end + alias_method :succ, :next + + # Return a new Date object that is +n+ months later than the current one. + # + # If the day-of-the-month of the current Date is greater than the last day of + # the target month, the day-of-the-month of the returned Date will be the last + # day of the target month. + def >> n + y, m = (year * 12 + (mon - 1) + n).divmod(12) + m, = (m + 1) .divmod(1) + d = mday + until jd2 = _valid_civil?(y, m, d) + d -= 1 + raise ArgumentError, 'invalid date' unless d > 0 + end + self + (jd2 - jd) + end + + # Return a new Date object that is +n+ months earlier than the current one. + # + # If the day-of-the-month of the current Date is greater than the last day of + # the target month, the day-of-the-month of the returned Date will be the last + # day of the target month. + def << (n) self >> -n end + + def next_month(n=1) self >> n end + def prev_month(n=1) self << n end + + def next_year(n=1) self >> n * 12 end + def prev_year(n=1) self << n * 12 end + def to_gregorian ::Date.jd jd end alias :gregorian :to_gregorian + def to_time + gregorian.to_time + end + + def to_date + self + end + + def to_datetime + DateTime.new! jd_to_ajd(jd, 0, 0), 0 + end + def strftime format='%Y/%m/%d' format. gsub('%%', 'PERCENT_SUBSTITUTION_MARKER'). gsub('%+', '%a %b %e %H:%M:%S %Z %Y'). gsub('%c', '%a %-d %B %Y'). @@ -159,127 +604,92 @@ gsub('%y', (year % 100).to_s). gsub('%m', '%02d' % month). gsub('%_m', '% 2d' % month). gsub('%-m', month.to_s). gsub('%^B', '%B'). - gsub('%B', Parsi::MONTHNAMES[month]). + gsub('%B', MONTHNAMES[month]). gsub('%h', '%b'). - gsub('%b', Parsi::ABBR_MONTHNAMES[month]). - gsub('%^b', Parsi::ABBR_MONTHNAMES[month].capitalize). + gsub('%b', ABBR_MONTHNAMES[month]). + gsub('%^b', ABBR_MONTHNAMES[month].capitalize). gsub('%d', '%02d' % day). gsub('%e', '% 2d' % day). gsub('%-d', day.to_s). gsub('%j', '%03d' % yday.to_s). - gsub('%A', Parsi::DAYNAMES[wday]). - gsub('%a', Parsi::ABBR_DAYNAMES[wday]). - gsub('%u', cwday.to_s). + gsub('%A', DAYNAMES[wday]). + gsub('%a', ABBR_DAYNAMES[wday]). gsub('%w', wday.to_s). gsub('%n', "\n"). gsub('%t', "\t"). gsub('PERCENT_SUBSTITUTION_MARKER', '%') end - def + days - raise TypeError.new 'expected numeric' unless days.is_a? Numeric - Date.jd jd + days - end - def - days - self + -days - end + # Step the current date forward +step+ days at a time (or backward, if +step+ is + # negative) until we reach +limit+ (inclusive), yielding the resultant date at each step. + def step limit, step=1 + return to_enum(:step, limit, step) unless block_given? - def >> monthes - raise TypeError.new 'expected numeric' unless monthes.is_a? Numeric - monthes = year * 12 + month + monthes - y = monthes / 12 - m = monthes % 12 - y -= 1 and m = 12 if m == 0 - d = day - d -= 1 until Date.valid? y, m, d - Date.new y, m, d - end - - def << monthes - self >> -monthes - end - - def <=> other - if other.respond_to? :jd - jd <=> other.jd - elsif other.is_a? Numeric - jd <=> other - else - raise ArgumentError.new "comparison of #{self.class} with #{other.class} failed" - end - end - - def step limit, by=1 date = self - comp_op = %w(== <= >=)[by <=> 0] + comp_op = %w(== <= >=)[step <=> 0] while date.send comp_op, limit yield date - date += by + date += step end self end - def upto max, &block + # Step forward one day at a time until we reach +max+ + # (inclusive), yielding each date as we go. + def upto max, &block # :yield: date step max, 1, &block end - def downto min, &block + # Step backward one day at a time until we reach +min+ + # (inclusive), yielding each date as we go. + def downto min, &block # :yield: date step min, -1, &block end - def next - self + 1 - end - alias :succ :next + # Is this Date equal to +other+? + # + # +other+ must both be a Date object, and represent the same date. + def eql? (other) self.class === other && self == other end - def next_day n=1 - self + n - end + # Calculate a hash value for this date. + def hash() ajd.hash end - def next_month n=1 - self >> n + # Return internal object state as a programmer-readable string. + def inspect + format('#<%s: %s (%s,%s)>', self.class, to_s, ajd, offset) end - def next_year n=1 - self >> (n * 12) - end + # Return the date as a human-readable string. + # + # The format used is YYYY-MM-DD. + def to_s() format('%.4d-%02d-%02d', year, mon, mday) end - def prev_day n=1 - self - n - end + # Dump to Marshal format. + def marshal_dump() [@ajd, @offset] end - def prev_month n=1 - self << n - end - - def prev_year n=1 - self << (n * 12) - end - - def shanbe? ; wday == 0 end - def yekshanbe? ; wday == 1 end - def doshanbe? ; wday == 2 end - def seshanbe? ; wday == 3 end - def chaharshanbe? ; wday == 4 end - def panjshanbe? ; wday == 5 end - def jomee? ; wday == 6 end - - private - def first_of_year - @first_of_year ||= Date.ordinal year - end + # Load from Marshal format. + def marshal_load(a) @ajd, @of, = a end end end class Date + class << self + # Creates a Date object corresponding to the specified Jalali Date +year+, +month+ and + # +day+. + def parsi year=0, month=1, day=1 + Parsi::Date.civil year, month, day + end + alias :jalali :parsi + end + + # Returns a Parsi::Date object representing same date in Jalali calendar def to_parsi - Parsi::Date.jd jd + Parsi::Date.new! ajd, offset end - alias :to_persian :to_parsi - alias :to_jalali :to_parsi - alias :parsi :to_parsi alias :jalali :to_parsi + alias :to_jalali :to_parsi + alias :to_persian :to_parsi end