Category talk:Wren-date

From Rosetta Code
Revision as of 09:25, 5 June 2020 by PureFox (talk | contribs) (Added source code for new 'Wren-date' module.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Source code

<lang ecmascript>/* Module "date.wren" */

import "/trait" for Comparable

/*

   Date represents a date (and the time within that date) as the number of milliseconds
   which have elapsed according to the Gregorian Proleptic calendar since midnight on 
   1st January, 0001. Dates before then or after the year 99,999 are not supported
   and leap seconds are ignored. Time zone data is also unsupported as Wren currently
   has no direct way of detecting its locale or the current local time. A Date object
   is immutable and its 'number' property can be used as a map key.
  • /

class Date is Comparable {

   // Private method to initialize date tables and default format.
   static init_() {
       __diy  = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]
       __diy2 = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]
       __days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
       __mths = ["January", "February", "March", "April", "May", "June", "July", "August",
                 "September", "October", "November", "December"]
       __ords = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Seventh", "Eighth",
                 "Ninth", "Tenth", "Eleventh", "Twelfth", "Thirteenth", "Fourteenth",
                 "Fifteenth", "Sixteenth", "Seventeenth", "Eighteenth", "Nineteenth",
                 "Twentieth", "Twenty-first", "Twenty-second", "Twenty-third", "Twenty-fourth",
                 "Twenty-fifth", "Twenty-sixth", "Twenty-seventh", "Twenty-eighth",
                 "Twenty-ninth", "Thirtieth", "Thirty-first"]
       __default = standard
   }
   // Constants.
   static zero      { Date.new(1, 1, 1) }
   static maximum   { Date.new(99999, 12, 31, 23, 59, 59, 999) }
   static unixEpoch { Date.new(1970, 1, 1) }
   // Predefined formats.
   static standard { "yyyy|-|mm|-|dd| |hh|:|MM|:|ss|.|ttt|" } // readable & sortable format
   static isoDate  { "yyyy|-|mm|-|dd|" }
   static usDate   { "mm|/|dd|/|yyyy"  }
   static ukDate   { "dd|/|mm|/|yyyy"  }
   static timeOnly { "hh|:|MM|:|ss"    }
   // Gets or sets the default format to be used by toString.
   static default { __default }
   static default=(fmt) { __default = (fmt is List || fmt is String) ? fmt : standard }
   // Determines whether a particular year is a leap year.
   static isLeapYear(y) {
       return y%4 == 0 && (y%100 != 0 || y%400 == 0)
   }
   // Returns the year length for a given date.
   static yearLength(y) { isLeapYear(y) ? 366 : 365 }
   // Returns the month length for a given date.
   static monthLength(y, m) { isLeapYear(y) ? __diy2[m] - __diy2[m-1] : __diy[m] - __diy[m-1] }
   // Returns the year day for a given date.
   static yearDay(y, m, d) { d + (isLeapYear(y) ? __diy2[m-1]  : __diy[m-1]) }
   // private helper method for isoWeek method
   static isoWeekOne_(y) {
       var dt = Date.new(y, 1, 4)
       var dow = dt.dayOfWeek
       return dt.addDays(1 - dow) // first Monday <= 4 January
   }
   // Returns the year and week number therein for a given date as per ISO 8601.
   // See https://secondboyet.com/Articles/PublishedArticles/CalculatingtheISOweeknumb.html
   static isoWeek(y, m, d) {
       var dt = Date.new(y, m, d)
       var week1
       if (dt >= Date.new(y, 12, 29)) { // ISO year might be next year
           week1 = isoWeekOne_(y + 1)
           if (dt < week1) {
               week1 = isoWeekOne_(y)  // it's this year
           } else {
               y = y + 1               // it's next year
           }
       } else {                        // ISO year might be previous year
           week1 = isoWeekOne_(y)
           if (dt < week1) {
               y = y - 1               // it's previous year
               week1 = isoWeekOne_(y)
           }
       }
       return [y, ((dt - week1).days / 7).truncate + 1]
   }
   // Parses a standard date string into a Date object, provided that ANY single character
   // separator may be used in place of dash, space, colon or dot and the following parts
   // may be omitted: the time as a whole, the secs/msecs part or just the msecs part.
   // Any trailing literal or unparseable text is ignored.
   static parse(str) {
       str = str.trim()
       var c = str.count
       if (c < 10) Fiber.abort("Unparseable date format string.")
       var y  = Num.fromString(str[0..3])
       var mo = Num.fromString(str[5..6])
       var d  = Num.fromString(str[8..9])
       if (c < 16) return Date.new(y, mo, d)
       var h  = Num.fromString(str[11..12])
       var mi = Num.fromString(str[14..15])
       if (c < 19) return Date.new(y, mo, d, h, mi)
       var s  = Num.fromString(str[17..18])
       if (c < 23) return Date.new(y, mo, d, h, mi, s)
       var ms = Num.fromString(str[20..22])
       return Date.new(y, mo, d, h, mi, s, ms)
   }
   // Private helper method to get the number of days from Date.zero up to the start of a year.
   static soyDays_(y) {
       if (y == 0) return 0
       y =  y - 1
       return y*365 + (y/4).floor - (y/100).floor + (y/400).floor
   }
   // Private helper method which fills an integer with leading zeros up to a given length.
   static zeroFill_(length, n) {
       n = "%(n)"
       return (n.count < length) ? "0" * (length - n.count) + n : n
   }
   // Private helper method which adds an ordinal suffix to a day number.
   static ord_(n) {
       var suffix = "th"
       if (n == 1 || n == 21 || n == 22) {
           suffix = "st"
       } else if (n == 2 || n == 22) {
           suffix = "nd"
       } else if (n == 3 || n == 23) {
           suffix = "rd"
       }
       return "%(n)" + suffix
   } 
   // Constructs a new Date object by passing it: the year, month, day, hour, minute, second
   // and millisecond of an instance in time. It is a runtime error to pass invalid values
   // though, for convenience, if the number of days is more than the month has, they will
   // wrap around to the following month.
   construct new(y, mo, d, h, mi, s, ms) {
       if (!(y is Num && y.isInteger && y >= 0 && y <= 99999)) {
           Fiber.abort("The year must be an integer in the range [0, 99999].")
       }
       if (!(mo is Num && mo.isInteger && mo >= 1 && mo <= 12)) {
           Fiber.abort("The month must be an integer in the range [1, 12].")
       }
       if (!(d is Num && d.isInteger && d >= 1 && d <= 31)) {
           Fiber.abort("The day must be an integer in the range [1, 31].")
       }
       if (!(h is Num && h.isInteger && h >= 0 && h <= 23)) {
           Fiber.abort("The hour must be an integer in the range [0, 23].")
       }
       if (!(mi is Num && mi.isInteger && mi >= 0 && mi <= 59)) {
           Fiber.abort("The minute must be an integer in the range [0, 59].")
       }
       if (!(s is Num && s.isInteger && s >= 0 && s <= 59)) {
           Fiber.abort("The second must be an integer in the range [0, 59].")
       }
       if (!(ms is Num && ms.isInteger && ms >= 0 && ms <= 999)) {
           Fiber.abort("The millisecond must be an integer in the range [0, 999].")
       }
       var days = Date.soyDays_(y)
       days = days + d - 1 + (Date.isLeapYear(y) ? __diy2[mo-1] : __diy[mo-1])
       _num = days * 86400000 + h * 3600000 + mi * 60000 + s * 1000 + ms
   }
   // Additional constructor for creating a Date object directly from a number of milliseconds.
   construct fromNum(num) {
       if (num < 0 || num > Date.maximum.number) Fiber.abort("Number is out of range.")
       _num = num
   }
   // Convenience methods to construct a Date object from a subset of its parameters.
   static new(y, mo, d, h, mi, s) { Date.new(y, mo, d, h, mi, s, 0) } 
   static new(y, mo, d, h, mi)    { Date.new(y, mo, d, h, mi, 0, 0) } 
   static new(y, mo, d)           { Date.new(y, mo, d, 0,  0, 0, 0) }
   static new(y)                  { Date.new(y,  1, 1, 0,  0, 0, 0) }
   // Gets the component parts of this date, as a list, from its number
   // and in the same order as the constructor parameters above.
   parts {
       var days = (_num/86400000).floor
       var time = _num % 86400000
       var h = (time/3600000).floor
       time = time % 3600000
       var mi = (time/60000).floor
       time = time % 60000
       var s = (time/1000).floor
       var ms = time % 1000
       var y = (days/365).floor + 1 // approximate year
       while(true) {
           var soyd = Date.soyDays_(y)
           var diff = days - soyd
           if (diff >= 0) {
               if (!Date.isLeapYear(y) && diff <= 364) {
                   var mo = 0
                   for (i in 0..11) {
                       if (diff < __diy[i]) {
                           mo = i
                           break
                       }
                   }
                   if (mo == 0) mo = 12
                   var d = diff - __diy[mo-1] + 1
                   return [y, mo, d, h, mi, s, ms]
               } else if (Date.isLeapYear(y) && diff <= 365) {
                   var mo = 0
                   for (i in 0..11) {
                       if (diff < __diy2[i]) {
                           mo = i
                           break
                       }
                   }
                   if (mo == 0) mo = 12
                   var d = diff - __diy2[mo-1] + 1 
                   return [y, mo, d, h, mi, s, ms]
               }
           }
           y = y - 1
       }
   }
   // Methods to get this date's basic properties.
   year     { parts[0] }
   month    { parts[1] }
   day      { parts[2] }
   hour     { parts[3] }
   minute   { parts[4] }
   second   { parts[5] }
   millisec { parts[6] }
   // Return a new Date object after adding positive (or negative) increments.
   addYears(y) { Date.new(year + y, month, day) }
   addMonths(mo) {
       var y = (mo/12).truncate
       if ((mo = mo%12) == 0) return addYears(y)
       var m = month
       if (mo >= 0) {
           if ((m + mo) <= 12) return Date.new(year + y, m + mo, day)
           return Date.new(year + y + 1, m + mo - 12, day)
       }
       if ((m + mo) >= 1) return Date.new(year + y, m + mo, day)
       return Date.new(year + y - 1, m + mo + 12, day)
   }
   addWeeks(w)      { Date.fromNum(_num + w * 86400000 * 7) }
   addDays(d)       { Date.fromNum(_num + d * 86400000)     }
   addHours(h)      { Date.fromNum(_num + h * 3600000)      }
   addMinutes(mi)   { Date.fromNum(_num + mi * 60000)       }
   addSeconds(s)    { Date.fromNum(_num + s * 1000)         }
   addMillisecs(ms) { Date.fromNum(_num + ms)               }
   // Returns the day of the year in which this date falls.
   dayOfYear { day + (Date.isLeapYear(year) ? __diy2[month-1] : __diy[month-1]) }
   dayOfWeek { (_num/86400000).floor % 7 + 1 } // as an integer (1 to 7), Monday = 1
   weekDay  { __days[dayOfWeek-1] } // as a string
   number { _num } // gets the number of miiliseconds since Date.zero
   unixTime { ((_num - Date.unixEpoch.number)/1000).truncate } // can be negative
   // Returns the ISO year and week in which this date falls.
   weekOfYear { Date.isoWeek(year, month, day) }
   // Compares this date with another one to enable comparison operators via Comparable trait.
   compare(other) { (_num - other.number).sign }
   // Constructs the string representation of this date from a 'fmt' list or string.
   // To treat a part of the format literally (and avoid a clash with a standard mask)
   // insert a '\v' within it which will otherwise be ignored.
   // If 'fmt' is a string then its components should be separated by '|'s. However, to use '|'
   // literally, replace it with a '\f' which will then be changed back during processing.
   format(fmt) {
       if (fmt is String) fmt = fmt.split("|")
       var str = ""
       for (f in fmt) {
           if (f == "y") {
               str = str + "%(parts[0])"                         // minimum digit year
           } else if (f == "yy") {
               str = str + Date.zeroFill_(2, parts[0] % 100)     // 2 digit year
           } else if (f == "yyy") {
               str = str + Date.zeroFill_(3, parts[0] % 1000)    // 3 digit year
           } else if (f == "yyyy") {
               str = str + Date.zeroFill_(4, parts[0] % 10000)   // 4 digit year
           } else if (f == "yyyyy") {
               str = str + Date.zeroFill_(5, parts[0])           // 5 digit year
           } else if (f == "m") {
               str = str + "%(parts[1])"                         // minimum digit month
           } else if (f == "mm") {
               str = str + Date.zeroFill_(2, parts[1])           // 2 digit month
           } else if (f == "mmm") {
               str = str + __mths[parts[1]-1][0..2]              // abbreviated month name
           } else if (f == "mmmm") {
               str = str + __mths[parts[1]-1]                    // full month name
           } else if (f == "d") {
               str = str + "%(parts[2])"                         // minimum digit day
            } else if (f == "dd") {
               str = str + Date.zeroFill_(2, parts[2])           // 2 digit day
           } else if (f == "ddd") {
               str = str + __days[parts[2]-1][0..2]              // abbreviated day name
           } else if (f == "dddd") {
               str = str + __days[parts[2]-1]                    // full day name
           } else if (f == "ooo") {
               str = str + Date.ord_(parts[2])                   // ordinal day number
           } else if (f == "oooo") {
               str = str + __ords[parts[2]-1]                    // full ordinal day name
           } else if (f == "h") {
               str = str + "%(parts[3])"                         // mimimum digit hour (24 hr)
           } else if (f == "hh") {
               str = str + Date.zeroFill_(2, parts[3])           // 2 digit hour (24 hr)
           } else if (f == "H") {
               var hr = parts[3] % 12
               if (hr == 0) hr = 12
               str = str + "%(hr)"                               // mimimum digit hour (12 hr)
           } else if (f == "HH") {
               var hr = parts[3] % 12
               if (hr == 0) hr = 12
               str = str + Date.zeroFill_(2, hr)                 // 2 digit hour (12 hr)
           } else if (f == "M") {
               str = str + "%(parts[4])"                         // minimum digit minute
           } else if (f == "MM") {
               str = str + Date.zeroFill_(2, parts[4])           // 2 digit minute
           } else if (f == "s") {
               str = str + "%(parts[5])"                         // minimum digit second
           } else if (f == "ss") {
               str = str + Date.zeroFill_(2, parts[5])           // 2 digit second
           } else if (f == "t") {
               str = str + "%(parts[6])"                         // minimum digit millisecond
           } else if (f == "ttt") {
               str = str + Date.zeroFill_(3, parts[6])           // 3 digit millisecond
           } else if (f == "am") {
               str = str + (parts[3] < 12) ? "am" : "pm"         // am/pm designation
           } else if (f == "AM") {
               str = str + (parts[3] < 12) ? "AM" : "PM"         // AM/PM designation
           } else {
               f = f.replace("\v", "").replace("\f", "|")
               str = str + f                                     // literal string
           }
       }
       return str
   }
   // Returns the string representation of this date using the default format.
   toString { format(Date.default) }
   // Returns the duration (positive or negative milliseconds) from this to another date.
   -(other) { Duration.new(_num - other.number) }

}

/*

   Duration represents a time interval (positive or negative) measured in milliseconds.
   It is immutable and any of its properties can therefore be used as a map key.
  • /

class Duration is Comparable {

   // Maximum safe integer (2^53 - 1) and hence duration
   static maximum { 9007199254740991 }
   // Constructs a new Duration object by passing it a number (positive or negative) of:
   // days, hours, minutes, seconds and milliseconds.
   construct new(d, h, mi, s, ms) {
       ms = d*86400000 + h*3600000 + mi*60000 + s*1000 + ms
       if (ms.abs > Duration.maximum) Fiber.abort("Duration is out of safe range.")
       _ms = ms
   }
   // Convenience method to construct a Duration object from just a number of milliseconds.
   static new(ms) { Duration.new(0, 0, 0, 0, ms) }
   // Gets the duration in various time intervals (as a floating point number).
   millisecs { _ms }
   seconds   { _ms/1000 }
   minutes   { _ms/60000 }
   hours     { _ms/3600000 }
   days      { _ms/86400000 }
   // Compares this duration with another one to enable comparison operators via Comparable trait.
   compare(other) { (_ms - other.millisecs).sign }
   // Duration arithmetic.
   +(other)  { Duration.new(_ms + other.millisecs) }
   -(other)  { Duration.new(_ms - other.millisecs) }
   // Returns the string representation of this duration.
   toString  { _ms.toString }

}

/* Stopwatch enables one to easily time events. */ class Stopwatch {

   // Times the execution of a fumction returning the duration it took to execute.
   static time(fn) {
       var sw = Stopwatch.new()
       fn.call()
       var dur = sw.duration
       sw.stop()
       return dur
   }
   // Creates a new Stopwatch object and starts it, recording the time.
   construct new() { _start = System.clock }
   // Returns the duration since 'start'.
   duration { Duration.new(((System.clock - _start)*1000).round) }
   // Returns the elapsed time since 'start' in milliseconds.
   elapsed  { duration.millisecs }
   // Prevents this Stopwatch object from being used again.
   stop() { _start = null }

}

// Type aliases for classes in case of any name clashes with other modules. var Date_Date = Date var Date_Duration = Duration var Date_Stopwatch = Stopwatch var Date_Comparable = Comparable // in case imported indirectly

// Initialize Date tables. Date.init_()</lang>