Advertisement
Guest User

Untitled

a guest
Aug 18th, 2017
453
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.58 KB | None | 0 0
  1. I stumbled upon this question on SO, and I found it very intriguing.
  2. https://stackoverflow.com/questions/45703404/custom-leap-year-logic-in-datetime
  3.  
  4. After spending some time to find a solution I had to give up, maybe someone can shed some light on this.
  5.  
  6. The problem is this:
  7.  
  8. I'm working on a fantasy text-based adventure, using the Curse library, for fun and decided that I wanted to customize Dates, from the standard Gregorian calendar. I've designed a new Date system that will stay relatively similar:
  9.  
  10. 12 months with different names
  11. The days in each month match up with the Gregorian calendar
  12. 7 days (with different names) a week
  13. 24 hours per day
  14. 60 minutes per hour
  15. 60 seconds per minute.
  16. leap years every 15 years
  17.  
  18. The different names I'm able to handle through I18n. The big difference ended up being that instead of a leap year being every 4 years, except centuries, I wanted leap years to occur every 15 years. So, seeing that Date has the *_leap? methods, I tried creating a subclass overriding those in hopes DateTime would check these methods when instantiating/adding to dates:
  19.  
  20. require 'date'
  21.  
  22. class CustomDate < DateTime
  23. class << self
  24. def leap?(year)
  25. year % 15 == 0
  26. end
  27.  
  28. def gregorian_leap?(year)
  29. leap?(year)
  30. end
  31.  
  32. def julian_leap?(year)
  33. leap?(year)
  34. end
  35. end
  36.  
  37. def leap?
  38. self.class.leap?(year)
  39. end
  40. end
  41.  
  42. CustomDate.leap?(2010) # => true
  43. Date.leap?(2010) # => false
  44. CustomDate.leap?(2000) # => false
  45. Date.leap?(2000) # => true
  46.  
  47. So far looking good, but then when actually trying to use these CustomDate nothing changed:
  48.  
  49. CustomDate.new(2000, 2, 29) # => hoped this would error because CustomDate.leap?(2000) is false...it didn't
  50. CustomDate.new(2010, 2, 29) # => errors, but was hoping this would work
  51.  
  52. Adding days:
  53.  
  54. # Was hoping this would be 2010-02-29
  55. CustomDate.new(2010, 2, 28) + 1
  56. # => #<CustomDate: 2010-03-01T00:00:00+00:00 ...>
  57.  
  58. # Was hoping this one would be 2000-03-01
  59. CustomDate.new(2000, 2, 28) + 1
  60. # => #<CustomDate: 2000-02-29T00:00:00+00:00 ...>
  61.  
  62. So, unless I'm missing something with this approach, this isn't going to work. Looking further, since Date is written in C, that kind of makes sense.
  63.  
  64. Looking at the source code for Date, there's a function c_gregorian_leap_p that seems to be the work horse for all these methods I overrode on the Ruby side. I'm thinking I'm going to need to write a c extension of some kind to override that function instead of the ruby one. I've never written C before, so before I get started down this path, I want to make sure I haven't missed anything in actual Ruby code.
  65.  
  66. TL;DR: Is there anyway to customize Date/DateTime in ruby to have custom leap year logic without writing some sort of C extension? With leap years being the only change to a Gregorian calendar, seems silly to rewrite Date/DateTime from scratch to change that one thing.
  67.  
  68.  
  69. My initial thought was to use monkey patching, but of course as he already mentioned when it come to actual usage of the said class things don't really work out.
  70.  
  71.  
  72. What I tried:
  73. - Monkey patch the Date.leap? but the new method does not use that.
  74. - Monkey patch the DateTime new method, like
  75.  
  76.  
  77. def self.new(*args)
  78. raise 'invalid date' if CustomDate.leap?(args[0])
  79. end
  80.  
  81.  
  82. But this only work for the new leap year not the old.
  83.  
  84. Does anyone have any idea of how to accomplish this only with Ruby, even if it might not be the best practice, or the only solution is to work directly with C.
  85.  
  86. Cheers, Bud.
  87. Andy Jones Andy.Jones@jameshall.co.uk via ruby-lang.org
  88.  
  89. 4:20 PM (23 hours ago)
  90.  
  91. to Ruby
  92.  
  93. Why not use composition, rather than inheritance? It seems as if it would be less work. Some of these base classes can’t safely be subclassed anyway.
  94.  
  95.  
  96.  
  97. class FantasyDate
  98.  
  99. WEEKDAYS = %w|Moonday Tewsday Wodensday Thorsday FirsDay Saturnday Sunday|
  100.  
  101.  
  102.  
  103. def initialize(date); @date = date; end
  104.  
  105.  
  106.  
  107. def weekday; WEEKDAYS[@date.strftime “%u”]; end
  108.  
  109.  
  110.  
  111. end
  112.  
  113.  
  114.  
  115.  
  116.  
  117. dt = FantasyDate(Date.today)
  118.  
  119. puts dt.weekday
  120.  
  121.  
  122.  
  123. …etc
  124.  
  125.  
  126.  
  127.  
  128. Click here to view Company Information and Confidentiality Notice.
  129.  
  130.  
  131. Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
  132. <http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk>
  133. Mugurel Chirica <chirica.mugurel@gmail.com>
  134.  
  135. 4:31 PM (23 hours ago)
  136.  
  137. to Ruby
  138. The main issue is with the leap? method.
  139.  
  140. CustomDate.leap?(2010) # => true
  141. Date.leap?(2010) # => false
  142. CustomDate.leap?(2000) # => false
  143. Date.leap?(2000) # => true
  144.  
  145. That's the expected behavior for leap year, but that should apply to object creation as well:
  146.  
  147. CustomDate.new(2000, 2, 29) # => hoped this would error because CustomDate.leap?(2000) is false...it didn't
  148. CustomDate.new(2010, 2, 29) # => errors, but was hoping this would work
  149.  
  150. I don't think composition would be able to solve this, since you can't create a invalid Date, it will throw ArgumentError.
  151. Andy Jones Andy.Jones@jameshall.co.uk via ruby-lang.org
  152.  
  153. 5:10 PM (22 hours ago)
  154.  
  155. to Ruby
  156.  
  157. If you have your own date class, you can make your own leap method?
  158.  
  159.  
  160.  
  161. def leap?
  162.  
  163. @date.year % 12 == 0
  164.  
  165. end
  166.  
  167.  
  168.  
  169. From: ruby-talk [mailto:ruby-talk-bounces@ruby-lang.org] On Behalf Of Mugurel Chirica
  170. Sent: 17 August 2017 16:31
  171. To: Ruby users
  172. Subject: Re: Custom Leap Year Logic in DateTime, used when creating new objects as well
  173.  
  174.  
  175. Click here to view Company Information and Confidentiality Notice.
  176.  
  177.  
  178. Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
  179. <http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk>
  180. Mugurel Chirica <chirica.mugurel@gmail.com>
  181.  
  182. 5:43 PM (22 hours ago)
  183.  
  184. to Ruby
  185.  
  186. CustomDate.new(2000, 2, 29)
  187.  
  188.  
  189. This creates a new valid date, but it should throw ArgumentError, since in the mentioned universe this date will be a leap year, and you don't have a 29th February in a leap year.
  190.  
  191. CustomDate.new(2010, 2, 29)
  192.  
  193. This throw ArgumentError since 2010 is a leap year, but in the said universe the date is valid so the expected behavior is that a new DateTime object will be created.
  194.  
  195. This extends to other dates operation, like addition etc.
  196.  
  197. Hope that clears things a bit.
  198.  
  199. Thank you.
  200. Andy Jones Andy.Jones@jameshall.co.uk via ruby-lang.org
  201.  
  202. 8:43 AM (7 hours ago)
  203.  
  204. to Ruby
  205. >>>>>>>>
  206. CustomDate.new(2000, 2, 29)
  207.  
  208. This creates a new valid date, but it should throw ArgumentError, since in the mentioned universe this date will be a leap year, and you don't have a 29th February in a leap year.
  209. <<<<<<<<
  210.  
  211. If this is your class, and it doesn’t raise an ArgumentError when you want it to, then … make it raise an ArgumentError?
  212.  
  213. ##########
  214. def initialize(year, month, day)
  215. fail ArgumentError if not_a_valid_fantasy_date(year, month, day)
  216. @basedate = Date.new(year, month, day)
  217. end
  218. #######
  219.  
  220.  
  221. >>>>>>>
  222. This extends to other dates operation, like addition etc.
  223. <<<<<<<
  224.  
  225. Again: if CustomDate is your class, and it only contains a real date internally, then you can implement these however you like.
  226.  
  227. You might want to check out the Forwardable module in Ruby's standard library, which might be helpful in passing on those methods you want for CustomDate which are unchanged from those in Ruby's Date. (You only need this if you are using composition.)
  228.  
  229.  
  230.  
  231. Click here to view Company Information and Confidentiality Notice.<http://www.jameshall.co.uk/index.php/small-print/email-disclaimer>
  232.  
  233. Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
  234. <http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk>
  235. Mugurel Chirica <chirica.mugurel@gmail.com>
  236.  
  237. 9:23 AM (6 hours ago)
  238.  
  239. to Ruby
  240. I understand, unfortunately that only addresses only the easy part of the problem which I already solved.
  241.  
  242. The difficult part of the problem is to create a date object that is valid in the new universe. Let me explain it a bit better to see the requirements.
  243.  
  244. Imagine that in the new universe the month can have 40 days, implementing the solution above will fail for CustomDate(2010, 1, 40) because that is not a valid Date in our universe, but the expected behavior is to have it create a new date since in our universe is a correct date.
  245.  
  246. [8] pry(main)> Date.new(2010, 2, 40) ArgumentError: invalid date from (pry):8:in `new' # That's the Date behaviour and the current behaviour for CustomDate
  247. [11] pry(main)> #<CustomDate: 2000-02-40T00:00:00+00:00 ((2451594j,0s,0n),+0s,2299161j)> # This is the expected behaviour
  248.  
  249. Andy Jones Andy.Jones@jameshall.co.uk via ruby-lang.org
  250.  
  251. 11:27 AM (4 hours ago)
  252.  
  253. to Ruby
  254.  
  255. >>>>>>>>Imagine that in the new universe the month can have 40 days, implementing the solution above will fail for CustomDate(2010, 1, 40) because that is not a valid Date in our universe, but the expected behavior is to have it create a new date since in our universe is a correct date.
  256.  
  257. [8] pry(main)> Date.new(2010, 2, 40) ArgumentError: invalid date from (pry):8:in `new' # That's the Date behaviour and the current behaviour for CustomDate
  258. [11] pry(main)> #<CustomDate: 2000-02-40T00:00:00+00:00 ((2451594j,0s,0n),+0s,2299161j)> # This is the expected behaviour
  259.  
  260. <<<<<<<<
  261.  
  262.  
  263.  
  264. Then it sounds as if you should not be trying to link your custom date object to a regular earth date at all. Ignore the Date class entirely.
  265.  
  266.  
  267.  
  268. For date maths, the easiest approach is probably to store the date internally as the number of days since an arbitrary “epoch start date”. Then subtracting a date from another date is trivial.
  269.  
  270.  
  271.  
  272. If you really need an equivalent earth date, then you can use the “days since epoch start” number to get that, too.
  273.  
  274.  
  275.  
  276. Don’t forget to implement a “ó” method. This will mean that your dates can be sorted AND if you include the comparable mixin, you get all the date comparing functionality -- `if mydate1 > mydate2 …` -- for free.
  277.  
  278.  
  279.  
  280. class FantasyDate
  281.  
  282. include Comparable
  283.  
  284. attr_reader :days_since_epoch, :year, :month, :day
  285.  
  286.  
  287.  
  288. def initialize(y, m, d)
  289.  
  290. @year, @month, @day = y, m, d
  291.  
  292. @days_since_epoch = calc_days(y,m,d)
  293.  
  294. end
  295.  
  296.  
  297.  
  298. def ó(other); @days_since_epoch ó other.days_since_epoch; end
  299.  
  300.  
  301.  
  302. def leap?
  303.  
  304. @year % 20 == 0 # or whatever
  305.  
  306. end
  307.  
  308. end
  309.  
  310.  
  311.  
  312. Click here to view Company Information and Confidentiality Notice.
  313.  
  314.  
  315. Unsubscribe: <mailto:ruby-talk-request@ruby-lang.org?subject=unsubscribe>
  316. <http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-talk>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement