diff --git a/CHANGELOG.md b/CHANGELOG.md index a416d62..e7c1173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Holiday definitions +## 3.0.0 + +Major semver bump as the format for custom methods has been changed to complete [issue-24](https://github.com/holidays/definitions/issues/24). Downstream consumers will need to update to be able to parse them. However there are **no behavior changes** with this update. + +In summary: we have switched to language-specific custom methods. Instead of a plain `source` field you will need a specific language implementation, e.g. `ruby`, `golang`, etc. + +Currently we only have `ruby` but we can now expand these definitions for use in other languages. Please see the [custom methods ADR](doc/architecture/adr-001.md) for more in-depth information on why this change was made. + +You can also view the updated ['Methods' section in the SYNTAX doc](doc/SYNTAX.md#methods) for more info and examples. + ## 2.5.3 * Add missing `observed` logic for 'St. Patricks Day' in `gb_nir` diff --git a/METHODS.yml b/METHODS.yml new file mode 100644 index 0000000..c3e80aa --- /dev/null +++ b/METHODS.yml @@ -0,0 +1,26 @@ +--- +methods: + easter: + arguments: year + orthodox_easter: + arguments: year + orthodox_easter_julian: + arguments: year + to_monday_if_sunday: + arguments: date + to_monday_if_weekend: + arguments: date + to_weekday_if_boxing_weekend: + arguments: date + to_weekday_if_boxing_weekend_from_year: + arguments: year + to_weekday_if_weekend: + arguments: date + calculate_day_of_month: + arguments: year, month, day, wday + to_weekday_if_boxing_weekend_from_year_or_to_tuesday_if_monday: + arguments: year + to_tuesday_if_sunday_or_monday_if_saturday: + arguments: date + lunar_to_solar: + arguments: year, month, day, region diff --git a/README.md b/README.md index bbe2186..fcc1cce 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,15 @@ Currently it is only used by the [existing Holidays gem](https://github.com/holi definitions and generates ruby classes for use in that gem. In the future it will be used by other languages in a similar manner. -*Please note* that this is *not* a gem. The validation process is written in ruby simply for convenience. The real +**Please note** that this is _not_ a gem. The validation process is written in ruby simply for convenience. The real stars of this show are the YAML files. -### Syntax +### Documentation -The definition syntax is a custom format developed over the life of this project. Please see -[our syntax doc](SYNTAX.md) for more information on how to format definitions. - -### How to contribute - -See our [contribution guidelines](CONTRIBUTING.md) for information on how to help out! + 1. [Syntax Guide](doc/SYNTAX.md) + 2. [Contribution Guidelines](doc/CONTRIBUTING.md) + 3. [Maintainer Guidelines](doc/MAINTAINERS.md) + 4. [Architecture Decision Records](doc/architecture/README.md) ### Credits diff --git a/ar.yaml b/ar.yaml index 5262938..0fe4775 100644 --- a/ar.yaml +++ b/ar.yaml @@ -72,6 +72,7 @@ months: - name: Navidad regions: [ar] mday: 25 + tests: - given: date: '2016-01-01' diff --git a/at.yaml b/at.yaml index 28937ec..f683c8d 100644 --- a/at.yaml +++ b/at.yaml @@ -57,6 +57,7 @@ months: - name: 2. Weihnachtstag regions: [at] mday: 26 + tests: - given: date: '2009-01-01' @@ -67,19 +68,16 @@ tests: - given: date: '2009-04-13' regions: ['at'] - options: 'informal' expect: name: 'Ostermontag' - given: date: '2009-05-21' regions: ['at'] - options: 'informal' expect: name: 'Christi Himmelfahrt' - given: date: '2009-06-01' regions: ['at'] - options: 'informal' expect: name: 'Pfingstmontag' - given: @@ -111,3 +109,8 @@ tests: regions: ['at'] expect: holiday: false + - given: + date: '2017-06-15' + regions: ['at'] + expect: + name: "Fronleichnam" diff --git a/au.yaml b/au.yaml index f020d74..d54faa3 100644 --- a/au.yaml +++ b/au.yaml @@ -1,5 +1,5 @@ # Australian holiday definitions for the Ruby Holiday gem. -# Updated: 2008-11-29. +# Updated: 2018-08-30 # Sources: # - http://en.wikipedia.org/wiki/Australian_public_holidays # - http://www.docep.wa.gov.au/lr/LabourRelations/Content/Wages%20and%20Conditions/Public%20Holidays/Public_Holidays.html @@ -178,7 +178,7 @@ months: methods: afl_grand_final: arguments: year - source: | + ruby: | case year when 2015 Date.civil(2015, 10, 2) @@ -192,7 +192,7 @@ methods: # celebrated twice in 2012 # in october again from 2016 arguments: year - source: | + ruby: | if year >= 2016 Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 10, 1, 1) elsif year == 2012 @@ -204,7 +204,7 @@ methods: # http://www.justice.qld.gov.au/fair-and-safe-work/industrial-relations/public-holidays/dates # in june until 2015 arguments: year - source: | + ruby: | if year <= 2015 Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 6, 2, 1) end @@ -212,7 +212,7 @@ methods: # http://www.justice.qld.gov.au/fair-and-safe-work/industrial-relations/public-holidays/dates # for 2013 to 2016 it was in October, otherwise it's in May arguments: year - source: | + ruby: | if year < 2013 || year >= 2016 Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 5, 1, 1) end @@ -220,7 +220,7 @@ methods: # http://www.justice.qld.gov.au/fair-and-safe-work/industrial-relations/public-holidays/dates # for 2013 to 2016 it was in October, otherwise it's in May arguments: year - source: | + ruby: | if year >= 2013 && year < 2016 Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 10, 1, 1) end @@ -228,20 +228,20 @@ methods: # http://www.justice.qld.gov.au/fair-and-safe-work/industrial-relations/public-holidays/dates # G20 day in brisbane, in 2014, on november 14 arguments: year - source: | + ruby: | year == 2014 ? 14 : nil hobart_show_day: # http://worksafe.tas.gov.au/__data/assets/pdf_file/0008/287036/Public_Holidays_2014.pdf # The Thursday before the fourth Saturday in October. arguments: year - source: | + ruby: | fourth_sat_in_oct = Date.civil(year, 10, Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 10, 4, :saturday)) fourth_sat_in_oct - 2 # the thursday before march_pub_hol_sa: # http://www.safework.sa.gov.au/show_page.jsp?id=2483#.VQ9Mfmb8-8E # The Holidays Act 1910 provides for the third Monday in May to be a public holiday. Since 2006 this public holiday has been observed on the second Monday in March through the issuing of a special Proclamation by the Governor. arguments: year - source: | + ruby: | if year < 2006 nil else @@ -251,7 +251,7 @@ methods: # http://www.safework.sa.gov.au/show_page.jsp?id=2483#.VQ9Mfmb8-8E # The Holidays Act 1910 provides for the third Monday in May to be a public holiday. Since 2006 this public holiday has been observed on the second Monday in March through the issuing of a special Proclamation by the Governor. arguments: year - source: | + ruby: | if year >= 2006 nil else @@ -259,6 +259,21 @@ methods: end tests: + - given: + date: "2017-04-14" + regions: ["au"] + expect: + name: "Good Friday" + - given: + date: "2017-04-15" + regions: ["au_nsw"] + expect: + name: "Easter Saturday" + - given: + date: ['2010-4-4', "2017-04-16"] + regions: ["au_nsw"] + expect: + name: "Easter Sunday" - given: date: '2013-10-07' regions: ["au_qld"] @@ -713,8 +728,18 @@ tests: regions: ["au_tas"] expect: name: "New Year's Day" + - given: + date: '2016-03-27' + regions: ["au_qld"] + expect: + holiday: false - given: date: '2017-04-16' regions: ["au_qld"] expect: name: "Easter Sunday" + - given: + date: "2014-11-14" + regions: ["au_qld_brisbane"] + expect: + name: "G20 Day" diff --git a/be_fr.yaml b/be_fr.yaml index 315c5e4..4ef6aeb 100644 --- a/be_fr.yaml +++ b/be_fr.yaml @@ -126,3 +126,28 @@ tests: options: ["informal"] expect: name: 'Noël' + - given: + date: '2017-4-16' + regions: ['be_fr'] + expect: + name: 'Pâques' + - given: + date: '2017-4-17' + regions: ['be_fr'] + expect: + name: 'Lundi de Pâques' + - given: + date: '2017-5-25' + regions: ['be_fr'] + expect: + name: 'Ascension' + - given: + date: '2017-6-4' + regions: ['be_fr'] + expect: + name: 'Pentecôte' + - given: + date: '2017-6-5' + regions: ['be_fr'] + expect: + name: 'Lundi de Pentecôte' diff --git a/be_nl.yaml b/be_nl.yaml index 4586195..20dd4d6 100644 --- a/be_nl.yaml +++ b/be_nl.yaml @@ -126,3 +126,28 @@ tests: options: ["informal"] expect: name: 'Kerstmis' + - given: + date: '2017-4-16' + regions: ['be_nl'] + expect: + name: 'Pasen' + - given: + date: '2017-4-17' + regions: ['be_nl'] + expect: + name: 'Paasmaandag' + - given: + date: '2017-5-25' + regions: ['be_nl'] + expect: + name: 'O.H. Hemelvaart' + - given: + date: '2017-6-4' + regions: ['be_nl'] + expect: + name: 'Pinksteren' + - given: + date: '2017-6-5' + regions: ['be_nl'] + expect: + name: 'Pinkstermaandag' diff --git a/ca.yaml b/ca.yaml index 6502ad8..e945dd8 100644 --- a/ca.yaml +++ b/ca.yaml @@ -106,7 +106,7 @@ months: regions: [ca_yt] mday: 21 year_ranges: - - after: 2017 + - after: 2017 7: - name: Canada Day regions: [ca] @@ -184,11 +184,12 @@ months: mday: 26 observed: to_weekday_if_boxing_weekend(date) type: informal + methods: ca_victoria_day: # Monday on or before May 24 arguments: year - source: | + ruby: | date = Date.civil(year,5,24) if date.wday > 1 date -= (date.wday - 1) diff --git a/ch.yaml b/ch.yaml index 245c61d..a121e54 100644 --- a/ch.yaml +++ b/ch.yaml @@ -125,11 +125,12 @@ months: - name: Restauration de la République regions: [ch_ge] mday: 31 + methods: ch_vd_lundi_du_jeune_federal: # Monday after the third Sunday of September arguments: year - source: | + ruby: | date = Date.civil(year,9,1) # Find the first Sunday of September until date.wday.eql? 0 do @@ -141,7 +142,7 @@ methods: ch_ge_jeune_genevois: # Thursday after the first Sunday of September arguments: year - source: | + ruby: | date = Date.civil(year,9,1) # Find the first Sunday of September until date.wday.eql? 0 do @@ -152,7 +153,7 @@ methods: ch_gl_naefelser_fahrt: # First Thursday of April. If the first Thursday of April is in the week before easter, then a week later. arguments: year - source: | + ruby: | date = Date.civil(year,4,1) # Find the first Thursday of April until date.wday.eql? 4 do diff --git a/cl.yaml b/cl.yaml index 6c80248..0894dbc 100644 --- a/cl.yaml +++ b/cl.yaml @@ -69,7 +69,7 @@ months: regions: [cl] year_ranges: - after: 2000 - function: columbus_day_cl(year) + function: columbus_day_cl(year) 11: - name: Día de Todos los Santos regions: [cl] @@ -81,43 +81,42 @@ months: - name: Navidad regions: [cl] mday: 25 + methods: st_peter_st_paul_cl: arguments: year # Nearest monday - source: | + ruby: | date = Date.civil(year, 6, 29) if [2,3,4].include?(date.wday) date -= (date.wday - 1) elsif date.wday == 5 date += 3 end - date columbus_day_cl: arguments: year # Nearest monday - source: | + ruby: | date = Date.civil(year, 10, 12) if [2,3,4].include?(date.wday) date -= (date.wday - 1) elsif date.wday == 5 date += 3 end - date other_churches_day_cl: arguments: year # If on tuesday, friday before, if on wednesday, next friday - source: | + ruby: | date = Date.civil(year, 10, 31) if date.wday == 2 date -= 4 elsif date.wday == 3 date += 2 end - date + tests: - given: date: '2014-01-01' diff --git a/co.yaml b/co.yaml index 2df2760..7848da0 100644 --- a/co.yaml +++ b/co.yaml @@ -14,7 +14,7 @@ methods: # Movable holiday: when they do not fall on a Monday, these holidays are observed the following Monday. to_following_monday_if_not_monday: arguments: date - source: | + ruby: | if date.wday > 1 date += ( 8 - date.wday ) elsif date.wday == 0 @@ -23,7 +23,7 @@ methods: date epiphany: arguments: year - source: | + ruby: | date = Date.civil( year, 1, 6 ) if date.wday > 1 date += ( 8 - date.wday ) @@ -33,7 +33,7 @@ methods: date saint_josephs_day: arguments: year - source: | + ruby: | date = Date.civil( year, 3, 19 ) if date.wday > 1 date += ( 8 - date.wday ) @@ -43,7 +43,7 @@ methods: date saint_peter_and_saint_paul: arguments: year - source: | + ruby: | date = Date.civil( year, 6, 29 ) if date.wday > 1 date += ( 8 - date.wday ) @@ -53,7 +53,7 @@ methods: date assumption_of_mary: arguments: year - source: | + ruby: | date = Date.civil( year, 8, 15 ) if date.wday > 1 date += ( 8 - date.wday ) @@ -63,7 +63,7 @@ methods: date columbus_day: arguments: year - source: | + ruby: | date = Date.civil( year, 10, 12 ) if date.wday > 1 date += ( 8 - date.wday ) @@ -73,7 +73,7 @@ methods: date all_saints_day: arguments: year - source: | + ruby: | date = Date.civil( year, 11, 1 ) if date.wday > 1 date += ( 8 - date.wday ) @@ -83,7 +83,7 @@ methods: date independence_of_cartagena: arguments: year - source: | + ruby: | date = Date.civil( year, 11, 11 ) if date.wday > 1 date += ( 8 - date.wday ) diff --git a/de.yaml b/de.yaml index afa4827..e654906 100644 --- a/de.yaml +++ b/de.yaml @@ -139,7 +139,7 @@ methods: de_buss_und_bettag: # Germany: Wednesday before November 23 arguments: year - source: | + ruby: | date = Date.civil(year,11,23) if date.wday > 3 date -= (date.wday - 3) diff --git a/CONTRIBUTING.md b/doc/CONTRIBUTING.md similarity index 61% rename from CONTRIBUTING.md rename to doc/CONTRIBUTING.md index 1f3407d..46f82e1 100644 --- a/CONTRIBUTING.md +++ b/doc/CONTRIBUTING.md @@ -6,10 +6,9 @@ In this repository we have all of the definitions that are used in holiday calcu Please read our [Code of Conduct](https://github.com/holidays/holidays/blob/master/CODE_OF_CONDUCT.md) before contributing. Everyone interacting with this project (or associated projects) is expected to abide by its terms. -## For definition updates +## Definition Updates -Our definitions are written in YAML. You can find a complete guide to our format in the [README](README.md). We take the YAML definitions and generate final definition files in the various projects -that are loaded at runtime for fast calculations. +Our definitions are written in YAML. You can find a complete guide to our format in the [syntax docs](SYNTAX.md). We take the YAML definitions and generate final definition files in the various projects that are loaded at runtime for fast calculations. Here are the steps to take once you have a good idea on what you want to change: @@ -18,8 +17,7 @@ Here are the steps to take once you have a good idea on what you want to change: * Run `make validate` to ensure that all updates match our definition format * Open a PR with your changes -Including documentation with your updates is very much appreciated. A simple Wikipedia entry or government link in the -comments alongside your changes would be perfect. +Including documentation with your updates is very much appreciated. A simple Wikipedia entry or government link in the comments alongside your changes would be perfect. Lastly, note that there are many 'meta' regions. For example, there are regions for Europe, Scandinavia, and North America. If your new region(s) falls into these areas consider adding them. You can find these 'meta' regions in `definitions/index.yaml`. @@ -29,12 +27,8 @@ Don't worry about versioning, we'll handle it on our end. ## Definition Validation -We maintain a `make validate` command to ensure that all YAML definitions match our internal specifications. This is to make -working with this repository as independent as possible from the other repositories (like the existing ruby repository). If -`make validate` passes then we ensure that anything consuming these files will receive 'correct' formats. +We maintain a `make validate` command to ensure that all YAML definitions match our internal specifications. This is to make working with this repository as independent as possible from the other repositories (like the existing ruby repository). If `make validate` passes then we ensure that anything consuming these files will receive 'correct' formats. -If you run into any weird `make validate` errors please open an issue or PR and highlight to what you are seeing. The -validation code is brand-new and might have issues. Maintainers will respond quickly to any open problems. +If you run into any weird `make validate` errors please open an issue or PR and highlight to what you are seeing. The validation code is brand-new and might have issues. Maintainers will respond quickly to any open problems. -If you would like to add to, update, or otherwise fix any of our specs then please fork and submit a PR like you would any -other change. Please note that we require 100% test coverage. Your builds will not pass if you fall below 100%. +If you would like to add to, update, or otherwise fix any of our specs then please fork and submit a PR like you would any other change. Please note that we require 100% test coverage. Your builds will not pass if you fall below 100%. diff --git a/MAINTAINERS.md b/doc/MAINTAINERS.md similarity index 72% rename from MAINTAINERS.md rename to doc/MAINTAINERS.md index f6de47f..3827d55 100644 --- a/MAINTAINERS.md +++ b/doc/MAINTAINERS.md @@ -32,14 +32,8 @@ will need to investigate further (contact a core member for assistance). that has the new version and associated changes. This is pretty open-ended! Include the information that you feel is important. Use past CHANGELOG updates as a guide. * Open a PR against the CHANGELOG branch and merge it (this may require another maintainer for safety) -* Once the updated CHANGELOG is merged, go to the [releases](https://github.com/holidays/definitions/blob/master/CHANGELOG.md) page -and create a new release. The release should point at the latest commit that contains the changes that you want included in -this release. If you just merged then you can just point at master. All release versions follow this -format: `vMAJOR.MINOR.PATCH`. This follows the normal [semver](https://github.com/holidays/definitions/blob/master/CHANGELOG.md) -rules. Look at recent releases to figure out what the new version should be. +* Once the updated CHANGELOG is merged, go to [releases](https://github.com/holidays/definitions/releases) and create a new release. It should point at the latest commit that contains the changes that you want included in this release. If you just merged then you can just point at master. All release versions follow this format: `vMAJOR.MINOR.PATCH`. This should follow normal [semver rules](https://semver.org/). -You don't need to list out the specific changes that were made on the release, you can just give a general overview. You can link to the -updated CHANGELOG that you did in a previous step. Example: [v2.2.0](https://github.com/holidays/definitions/releases/tag/v2.2.0) +You don't need to list out the specific changes that were made on the release description. You can just give a general overview and then link to the updated CHANGELOG that you did in a previous step. Example: [v2.2.0](https://github.com/holidays/definitions/releases/tag/v2.2.0) -Once the release is created in Github you are done! The definitions have been 'released' and downstream projects (right now just ruby) -can reference them without issues. See the downstream maintainers guides for information on how to release updates. +Once the release is created in Github you are done! The definitions have been 'released' and downstream projects (right now just ruby) can reference them without issues. See the maintainers guides in downstream projects for information on how to release updates for each language. diff --git a/SYNTAX.md b/doc/SYNTAX.md similarity index 77% rename from SYNTAX.md rename to doc/SYNTAX.md index 573b283..be006c1 100644 --- a/SYNTAX.md +++ b/doc/SYNTAX.md @@ -1,6 +1,6 @@ -# Holiday Gem Definition Syntax +# Holiday Definition Syntax -All holidays are defined in these YAML files. These definition files have three main top-level properties: +The definition syntax is a custom format developed over the life of this project. All holidays are defined in these YAML files. These definition files have three main top-level properties: * `months` - this is the meat! All definitions for months 1-12 are defined here * `methods` - this contains any custom logic that your definitions require @@ -93,7 +93,7 @@ If a user submits: Holidays.on(Date.civil(2016, 9, 1), :fr) ``` -then they will not see the holiday. However, if they submit: +Then they will not see the holiday. However, if they submit: ```ruby Holidays.on(Date.civil(2016, 9, 1), :fr, :informal) @@ -228,15 +228,23 @@ Holidays.on(Date.civil(1995, 7, 1), :jp) ## Methods -In addition to defining holidays by day or week, you can create custom methods to calculate a date. These should be placed under the `methods` property. Methods named in this way can then be referenced by entries in the `months` property. +Sometimes you need to perform a complex calculation to determine a holiday. To facilitate this we allow for users to specify custom methods to calculate a date. These should be placed under the `methods` property. Methods named in this way can then be referenced by entries in the `months` property. -For example, Canada celebrates Victoria Day, which falls on the Monday on or before May 24. So, under the `methods` property we create a custom method that returns a Date object. +#### Important note -``` +One thing to note is that these methods are _language specific_ at this time, meaning we would have one for ruby, one for golang, etc. Coming up with a standardized way to represent the logic in the custom-written methods proved to be very difficult. This is a punt until we can come up with a better solution. + +Please feel free to only add the custom method source in the language that you choose. It will be up to downstream maintainers to ensure that their language has an implementation. So if you only want to add it in ruby please just do that! + +### Method Example + +Canada celebrates Victoria Day, which falls on the Monday on or before May 24. Under the `methods` property we would create a custom method for ruby that returns a Date object: + +```yaml methods: ca_victoria_day: arguments: year - source: | + ruby: | date = Date.civil(year, 5, 24) if date.wday > 1 date -= (date.wday - 1) @@ -247,25 +255,57 @@ methods: date ``` -This would be represented in `months` entry as: +This could then be used in a `months` entry: -``` +```yaml 5: - name: Victoria Day regions: [ca] function: ca_victoria_day(year) ``` -If a holiday can occur in different months (e.g. Easter) it can go in the '0' month. +### Available arguments + +You may only specify the following values for arguments into a custom method: `date`, `year`, `month`, `day`, `region` + +Correct example: +```yaml +1: +- name: Custom Method + regions: [us] + function: custom_method(year, month, day) ``` + +The following will return an error since `week` is not a recognized argument: + +```yaml +1: +- name: Custom Method + regions: [us] + function: custom_method(week) +``` + +#### Whaa? Why do you restrict what I can pass in? + +This was done as an attempt to make it easier for the downstream projects to parse and use the custom methods. They have to be able to pass in the required data so we limit it to make that process easier. + +We can add to this list if your custom logic needs something else! Open an issue with your use case and we can discuss it. + +### Methods without a fixed month + +If a holiday does not have a fixed month (e.g. Easter) it should go in the '0' month: + +```yaml 0: - name: Easter Monday regions: [ca] function: easter(year) ``` -There are pre-existing methods for highly-used calculations. They are: +### Pre-existing methods + +There are pre-existing methods for highly-used calculations. You can reference these methods in your definitions as you would a custom method that you have written: * `easter(year)` - calculates Easter via Gregorian calendar for a given year * `orthodox_easter(year)` - calculates Easter via Julian calendar for a given year @@ -288,36 +328,13 @@ There are pre-existing methods for highly-used calculations. They are: Use the `function_modifier` property, which can be positive or negative, to modify the result of the function. -In addition, you may only specify the following values for arguments into a custom method: `date`, `year`, `month`, `day`. - -If attempt to specify anything else then you will receive an error on definition generation. This is because these are the only values that are available to -call into the custom methods will calculating the result of a function. - -Correct example: - -``` -1: -- name: Custom Method - regions: [us] - function: custom_method(year, month, day) -``` - -If you do the following: - -``` -1: -- name: Custom Method - regions: [us] - function: custom_method(week) -``` - -This will result in an error since `week` is not a recognized method argument. - ### Calculating observed dates Users can specify that this gem only return holidays on their 'observed' day. This can be especially useful if they are using this gem for business-related logic. If you wish for your definitions to allow for this then you can add the `observed` property to your entry. This requires a method to help calculate the observed day. -Several built-in methods are available for holidays that are observed on varying dates. For example, for a holiday that is observed on Monday if it falls on a weekend you could write: +Several built-in methods are available for holidays that are observed on varying dates. + +For example, for a holiday that is observed on Monday if it falls on a weekend you could write: ``` 7: @@ -327,14 +344,13 @@ Several built-in methods are available for holidays that are observed on varying observed: to_monday_if_weekend(date) ``` -If a user does not specify `observed` when calling the gem then 1/1 will be the date found for 'Canada Day', regardless of whether it falls on a Saturday or Sunday. If a user specifies 'observed' then it will show as the following Monday if the date falls on a Saturday or Sunday. +If a user does not specify `observed` in the options then 7/1 will be the date found for 'Canada Day', regardless of whether it falls on a Saturday or Sunday. If a user specifies 'observed' then it will show as the following Monday if the date falls on a Saturday or Sunday. ## Tests -All definition files should have tests included. At this time we do not enforce any rules on coverage or numbers of tests. -However, in general, PRs will not be accepted if they are devoid of tests that cover the changes in question. +All definition files should have tests included. At this time we do not enforce any rules on coverage or numbers of tests. However, in general, PRs will not be accepted if they are devoid of tests that cover the changes in question. -The format is a straightforward 'given then expect'. Here is a simple example: +The format is a straightforward 'given/expect'. Here is a simple example: ```yaml - given: @@ -356,7 +372,9 @@ Here are format details: One or the other of the `expect` keys is required. If you do not specify a `name` then you should set `holiday: false`. -Here are some more examples. First example shows multiple dates, multiple regions, additional options, and an expectation that the result will be the named holiday. +#### Test Examples + +First example shows multiple dates, multiple regions, additional options, and an expectation that the result will be the named holiday: ```yaml - given: diff --git a/doc/architecture/README.md b/doc/architecture/README.md new file mode 100644 index 0000000..b015c8a --- /dev/null +++ b/doc/architecture/README.md @@ -0,0 +1,14 @@ +# Architecture Decision Records + +Here we document decisions we made regarding high level architecture/design of this repository. + +For details on what ADR is and why it's important, please reference the following blog posts: + + - https://product.reverb.com/documenting-architecture-decisions-the-reverb-way-a3563bb24bd0 + - http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions + +Please note that we only began keeping ADRs for this project in October of 2018. Decisions made before that are not covered. + +## Table of contents + + 1. [Language specific custom methods](adr-001.md) diff --git a/doc/architecture/adr-001.md b/doc/architecture/adr-001.md new file mode 100644 index 0000000..65e6401 --- /dev/null +++ b/doc/architecture/adr-001.md @@ -0,0 +1,86 @@ +# ADR 1: Custom Methods Format Change + +## Context + +We would like these definitions to be usable by any language. The original `holidays` project was written purely in `ruby` but the definitions were generally plain `YAML`. + +The issue is that `ruby` has been sprinkled into the otherwise plain `YAML` when it made sense with no plan for use outside of `ruby`. This makes sense when you are never planning on using the `YAML` files in other languages. + +Over time we have [been working](https://github.com/holidays/definitions/issues/7) to make the syntax more generic so that other language implementations could consume them. The last hurdle was custom methods. + +An example of the original format: + +```yaml +methods: + ca_victoria_day: + arguments: year + source: | + date = Date.civil(year, 5, 24) + if date.wday > 1 + date -= (date.wday - 1) + elsif date.wday == 0 + date -= 6 + end + + date +``` + +As you can see the actual function is just plain `ruby`. + +After lots of trial and error I have decided that I cannot see a generic format for this logic that would satisfy all use cases for existing custom methods in our definitions. While some custom methods are relatively simple `if/else` statements there are many that are much more complicated. + +An example of a 'complicated' custom method from the `ch` (Swiss) region: + +```yaml + ch_vd_lundi_du_jeune_federal: + # Monday after the third Sunday of September + arguments: year + ruby: | + date = Date.civil(year,9,1) + # Find the first Sunday of September + until date.wday.eql? 0 do + date += 1 + end + # There are 15 days between the first Sunday + # and the Monday after the third Sunday + date + 15 +``` + +The logic itself is not hard to follow but coming up with a generic way to phrase this seems like a complex problem. Every attempt that was made devolved into very complex parsers of nested `YAML` so that we correctly handled each new edge case that appeared. It was very slow going and the complexity was growing and growing. + +Additionally, having a complex `YAML` syntax for custom methods would require each downstream repository to implement the 'standard'. That seems pretty scary to think about maintaining. + +The other option is to just make each future language provide their own implementations. + +## Decision + +The decision is to simply require language-specific implementations of custom methods. Since all custom methods are currently in `ruby` we are changing every `source` field to `ruby`. In the future new languages will need to provide their own implementations. For example, we could add a `golang` or `swift` section next to the existing `ruby` section. + +There are three significant advantages: + + - It is very easy to understand + - All holidays using custom methods have tests so each downstream project will have built-in protection in case a bug is introduced in only one language implementation + - It is very easy to implement for the current `ruby` implementation (which is our only project currently) + +There are significant downsides: + + - Possible divergence between languages due to separate implementations, causing confusion and frustration + - More pressure on maintainers to handle the various implementations, ensuring they can build without issues when new custom methods are added + - Confusion for new contributors who may only be comfortable in a single language + - New downstream languages will have a higher hurdle to overcome since they will need to implement the existing logic in their own language + +In the end I don't want to hold things up because of _possible_ new language implementations that might show up in the future. I personally want to create a new `golang` version of `holidays` but beyond that maybe no one else will consume these definitions! + +If the `holidays` projects become wildly popular in the future and this becomes a huge problem then I can address it with the (presumably huge) community to find a solution. + +## Consequences + +We might lose contributions due to confusion or fear. + +We might burn out maintainers if the juggling of languages becomes too much of a burden. + +This puts more pressure for the completion of an updated [test framework](https://github.com/holidays/definitions/issues/42) for downstream repositories. + +## Status + +Accepted. diff --git a/fedex.yaml b/fedex.yaml index fc9a299..4b70880 100644 --- a/fedex.yaml +++ b/fedex.yaml @@ -42,10 +42,11 @@ months: - name: New Year's Eve regions: [fedex] mday: 31 + methods: day_after_thanksgiving: arguments: year - source: | + ruby: | Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 11, 4, 4) + 1 tests: diff --git a/fi.yaml b/fi.yaml index 0ef86ee..0a7447f 100644 --- a/fi.yaml +++ b/fi.yaml @@ -59,11 +59,12 @@ months: - name: Tapaninpäivä regions: [fi] mday: 26 + methods: fi_juhannusaatto: # Finland: Mid-summer eve (Friday between June 19–25) arguments: year - source: | + ruby: | date = Date.civil(year,6,19) if date.wday > 5 #if 19.6 is saturday date += 6 @@ -74,14 +75,14 @@ methods: fi_juhannuspaiva: # Finland: Mid-summer (Saturday between June 20–26) arguments: year - source: | + ruby: | date = Date.civil(year,6,20) date += (6 - date.wday) date fi_pyhainpaiva: # Finland: All Saint's Day (Saturday between Oct 31 and Nov 6) arguments: year - source: | + ruby: | date = Date.civil(year,10,31) date += (6 - date.wday) date diff --git a/hk.yaml b/hk.yaml index 59fb3b8..a884b98 100644 --- a/hk.yaml +++ b/hk.yaml @@ -71,7 +71,7 @@ months: methods: cn_new_lunar_day: arguments: year - source: | + ruby: | month_day = case year when 1930, 1949, 1987, 2025, 2063, 2082, 2101, 2112, 2131, 2150, 2207, 2245, 2253, 2283, 2321 [1, 29] diff --git a/is.yaml b/is.yaml index 766ac88..cb101a5 100644 --- a/is.yaml +++ b/is.yaml @@ -114,7 +114,7 @@ methods: is_sumardagurinn_fyrsti: # Iceland: first day of summer (Thursday after 18 April) arguments: year - source: | + ruby: | date = Date.civil(year,4,18) if date.wday < 4 date += (4 - date.wday) diff --git a/jp.yaml b/jp.yaml index 9bb138b..62a62ac 100644 --- a/jp.yaml +++ b/jp.yaml @@ -178,11 +178,12 @@ months: methods: jp_health_sports_day_substitute: arguments: year - source: | + ruby: | Holidays::Factory::Definition.custom_methods_repository.find("jp_substitute_holiday(year, month, day)").call(year, 10, Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 10, 2, 1)) + jp_vernal_equinox_day: arguments: year - source: | + ruby: | day = case year when 1851..1899 @@ -199,18 +200,21 @@ methods: day += 0.242194 * (year - 1980) - ((year - 1980)/4).floor day = day.floor Date.civil(year, 3, day) + jp_vernal_equinox_day_substitute: arguments: year - source: | + ruby: | date = Holidays::Factory::Definition.custom_methods_repository.find("jp_vernal_equinox_day(year)").call(year) Holidays::Factory::Definition.custom_methods_repository.find("jp_substitute_holiday(year, month, day)").call(year, date.month, date.mday) + jp_marine_day_substitute: arguments: year - source: | + ruby: | Holidays::Factory::Definition.custom_methods_repository.find("jp_substitute_holiday(year, month, day)").call(year, 7, Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 7, 3, 1)) + jp_national_culture_day: arguments: year - source: | + ruby: | day = case year when 1851..1899 @@ -227,41 +231,49 @@ methods: day += 0.242194 * (year - 1980) - ((year - 1980)/4).floor day = day.floor Date.civil(year, 9, day) + jp_national_culture_day_substitute: arguments: year - source: | + ruby: | date = Holidays::Factory::Definition.custom_methods_repository.find("jp_national_culture_day(year)").call(year) Holidays::Factory::Definition.custom_methods_repository.find("jp_substitute_holiday(year, month, day)").call(year, date.month, date.mday) + jp_citizens_holiday: arguments: year - source: | + ruby: | ncd = Holidays::Factory::Definition.custom_methods_repository.find("jp_national_culture_day(year)").call(year) if ncd.wday == 3 ncd - 1 else nil end + jp_mountain_holiday: arguments: year - source: | + ruby: | Date.civil(year, 8, 11) + jp_mountain_holiday_substitute: arguments: year - source: | + ruby: | date = Holidays::Factory::Definition.custom_methods_repository.find("jp_mountain_holiday(year)").call(year) Holidays::Factory::Definition.custom_methods_repository.find("jp_substitute_holiday(year, month, day)").call(year, date.month, date.mday) + jp_respect_for_aged_holiday_substitute: arguments: year - source: | + ruby: | Holidays::Factory::Definition.custom_methods_repository.find("jp_substitute_holiday(year, month, day)").call(year, 9, Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 9, 3, 1)) + jp_substitute_holiday: arguments: year, month, day - source: | + ruby: | date = Date.civil(year, month, day) date.wday == 0 ? (Holidays::Factory::Definition.custom_methods_repository.find("jp_next_weekday(date)").call(date+1)) : nil jp_next_weekday: arguments: date - source: | + ruby: | + # This suuuucks. I have no idea how to make this not reach into our interal ruby API to do this. + # I'm punting, I'll come back to this. is_holiday = Holidays::JP.holidays_by_month[date.month].any? do |holiday| holiday[:mday] == date.day end @@ -606,4 +618,4 @@ tests: date: '2020-02-23' regions: ["jp"] expect: - name: "天皇誕生日" \ No newline at end of file + name: "天皇誕生日" diff --git a/lib/validation/custom_method_validator.rb b/lib/validation/custom_method_validator.rb index e89df7d..ad643ef 100644 --- a/lib/validation/custom_method_validator.rb +++ b/lib/validation/custom_method_validator.rb @@ -10,7 +10,7 @@ def call(methods) raise Errors::InvalidCustomMethod unless valid_name?(name) && valid_arguments?(method['arguments']) && - valid_source?(method['source']) + valid_source?(method['ruby']) end true diff --git a/lib/validation/test_validator.rb b/lib/validation/test_validator.rb index 84da607..130673e 100644 --- a/lib/validation/test_validator.rb +++ b/lib/validation/test_validator.rb @@ -10,12 +10,7 @@ def call(tests) err!("Tests must be an array") unless tests.is_a?(Array) tests.each do |t| - begin - validate_given!(t["given"]) - validate_expect!(t["expect"]) - rescue Errors::InvalidTest => e - raise Errors::InvalidTest.new("#{e.message} - #{t.inspect}") - end + validate!(t) end true @@ -27,6 +22,13 @@ def err!(msg) raise Errors::InvalidTest.new(msg) end + def validate!(t) + validate_given!(t["given"]) + validate_expect!(t["expect"]) + rescue Errors::InvalidTest => e + raise Errors::InvalidTest.new("#{e.message} - #{t.inspect}") + end + def validate_given!(g) err!("Test must contain given key") if g.nil? @@ -59,14 +61,16 @@ def validate_date_values!(given) given["date"] = [ given["date"] ] unless given["date"].is_a?(Array) given["date"].each do |d| - begin - DateTime.parse(d) - rescue TypeError, ArgumentError, NoMethodError - err!("Test must contain valid date, date value was: '#{d}") - end + parse_date!(d) end end + def parse_date!(d) + DateTime.parse(d) + rescue TypeError, ArgumentError, NoMethodError + err!("Test must contain valid date, date value was: '#{d}") + end + def validate_expect!(e) err!("Test must contain expect key") if e.nil? diff --git a/nz.yaml b/nz.yaml index c9eae5a..95e2b21 100644 --- a/nz.yaml +++ b/nz.yaml @@ -254,7 +254,7 @@ tests: methods: closest_monday: arguments: date - source: | + ruby: | if [1, 2, 3, 4].include?(date.wday) date -= (date.wday - 1) elsif 0 == date.wday @@ -265,9 +265,9 @@ methods: date previous_friday: arguments: date - source: | + ruby: | date - 3 next_week: arguments: date - source: | + ruby: | date + 7 diff --git a/ph.yaml b/ph.yaml index b4343e4..2fff251 100644 --- a/ph.yaml +++ b/ph.yaml @@ -73,7 +73,7 @@ methods: ph_heroes_day: # last Monday of August arguments: year - source: | + ruby: | date = Date.new(year, 8, -1) if date.wday != 1 diff --git a/pl.yaml b/pl.yaml index bb0b91b..8644e27 100644 --- a/pl.yaml +++ b/pl.yaml @@ -177,12 +177,12 @@ methods: pl_trzech_kroli: # Poland: January 6 is holiday since 2011 arguments: year - source: | + ruby: | year >= 2011 ? 6 : nil pl_trzech_kroli_informal: # Poland: January 6 wasn't holiday before 2011 arguments: year - source: | + ruby: | year < 2011 ? 6 : nil tests: diff --git a/se.yaml b/se.yaml index a149ac1..33e8e9f 100644 --- a/se.yaml +++ b/se.yaml @@ -79,14 +79,14 @@ methods: se_midsommardagen: # Sweden: Mid-summer (Saturday between June 20–26) arguments: year - source: | + ruby: | date = Date.civil(year,6,20) date += (6 - date.wday) date se_alla_helgons_dag: # Sweden: All Saint's Day (Saturday between Oct 31 and Nov 6) arguments: year - source: | + ruby: | date = Date.civil(year,10,31) date += (6 - date.wday) date diff --git a/spec/validation/custom_method_validator_spec.rb b/spec/validation/custom_method_validator_spec.rb index c1f53e3..82f60e8 100644 --- a/spec/validation/custom_method_validator_spec.rb +++ b/spec/validation/custom_method_validator_spec.rb @@ -6,7 +6,7 @@ { 'test' => { 'arguments' => "date,year,month,day", - 'source' => "some source", + 'ruby' => "some source", } } } @@ -47,12 +47,12 @@ context 'source' do it 'returns false if nil' do - methods['test']['source'] = nil + methods['test']['ruby'] = nil expect { subject.call(methods) }.to raise_error(Definitions::Errors::InvalidCustomMethod) end it 'returns false if empty' do - methods['test']['source'] = "" + methods['test']['ruby'] = "" expect { subject.call(methods) }.to raise_error(Definitions::Errors::InvalidCustomMethod) end end diff --git a/tr.yaml b/tr.yaml index 38eb292..f3059f1 100644 --- a/tr.yaml +++ b/tr.yaml @@ -77,7 +77,7 @@ months: methods: ramadan_feast: arguments: year - source: | + ruby: | begin_of_ramadan_feast = { '2014' => Date.civil(2014, 7, 28), '2015' => Date.civil(2015, 7, 17), @@ -89,7 +89,7 @@ methods: begin_of_ramadan_feast[year.to_s] sacrifice_feast: arguments: year - source: | + ruby: | begin_of_sacrifice_feast = { '2014' => Date.civil(2014, 10, 4), '2015' => Date.civil(2015, 9, 24), diff --git a/ups.yaml b/ups.yaml index b37f723..7d32431 100644 --- a/ups.yaml +++ b/ups.yaml @@ -46,7 +46,7 @@ months: methods: day_after_thanksgiving: arguments: year - source: | + ruby: | Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 11, 4, 4) + 1 tests: diff --git a/us.yaml b/us.yaml index 49af7d0..90d52d5 100644 --- a/us.yaml +++ b/us.yaml @@ -313,12 +313,12 @@ months: methods: christmas_eve_holiday: arguments: date - source: | + ruby: | beginning_of_month = Date.civil(date.year, date.month, 1) (date.saturday? || date.sunday?) ? date.downto(beginning_of_month).find {|d| d if d.wday == 5} : date rosh_hashanah: arguments: year - source: | + ruby: | rosh_hashanah_dates = { '2014' => Date.civil(2014, 9, 25), '2015' => Date.civil(2015, 9, 14), @@ -331,7 +331,7 @@ methods: rosh_hashanah_dates[year.to_s] yom_kippur: arguments: year - source: | + ruby: | yom_kippur_dates = { '2014' => Date.civil(2014, 10, 4), '2015' => Date.civil(2015, 9, 23), @@ -345,14 +345,14 @@ methods: georgia_state_holiday: # Monday before that holiday arguments: year, month - source: | + ruby: | beginning_of_month = Date.civil(year, month, 1) state_holiday = Date.civil(year, month, 26) state_holiday.downto(beginning_of_month).find {|date| date if date.wday == 1 } lee_jackson_day: # Friday before Martin Luther King, Jr. Day arguments: year, month - source: | + ruby: | day_of_holiday = Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, month, 3, 1) beginning_of_month = Date.civil(year, month, 1) king_day = Date.civil(year, month, day_of_holiday) @@ -360,16 +360,16 @@ methods: election_day: # Tuesday after the first Monday of November arguments: year - source: | + ruby: | Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 11, 1, 1) + 1 us_inauguration_day: # January 20, every fourth year, following Presidential election arguments: year - source: | + ruby: | year % 4 == 1 ? 20 : nil day_after_thanksgiving: arguments: year - source: | + ruby: | Holidays::Factory::DateCalculator.day_of_month_calculator.call(year, 11, 4, 4) + 1 tests: