├── .github └── workflows │ └── tests.yml ├── .gitignore ├── CODEOWNERS ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── rules ├── br │ ├── br.go │ ├── br_test.go │ ├── casual_date.go │ ├── casual_test.go │ ├── casual_time.go │ ├── deadline.go │ ├── deadline_test.go │ ├── exact_month_date.go │ ├── exact_month_date_test.go │ ├── hour.go │ ├── hour_minute.go │ ├── hour_minute_test.go │ ├── hour_test.go │ ├── past_time.go │ ├── past_time_test.go │ ├── weekday.go │ └── weekday_test.go ├── common │ ├── common.go │ ├── common_test.go │ ├── slash_dmy.go │ └── slash_dmy_test.go ├── context.go ├── en │ ├── casual_date.go │ ├── casual_test.go │ ├── casual_time.go │ ├── deadline.go │ ├── deadline_test.go │ ├── en.go │ ├── en_test.go │ ├── exact_month_date.go │ ├── exact_month_date_test.go │ ├── hour.go │ ├── hour_minute.go │ ├── hour_minute_test.go │ ├── hour_test.go │ ├── past_time.go │ ├── past_time_test.go │ ├── weekday.go │ └── weekday_test.go ├── nl │ ├── casual_date.go │ ├── casual_test.go │ ├── casual_time.go │ ├── deadline.go │ ├── deadline_test.go │ ├── exact_month_date.go │ ├── exact_month_date_test.go │ ├── hour.go │ ├── hour_minute.go │ ├── hour_minute_test.go │ ├── hour_test.go │ ├── nl.go │ ├── nl_test.go │ ├── past_time.go │ ├── past_time_test.go │ ├── weekday.go │ └── weekday_test.go ├── ru │ ├── casual_date.go │ ├── casual_test.go │ ├── casual_time.go │ ├── date.go │ ├── date_test.go │ ├── deadline.go │ ├── deadline_test.go │ ├── dot_date_time.go │ ├── dot_date_time_test.go │ ├── hour.go │ ├── hour_minute.go │ ├── hour_minute_test.go │ ├── hour_test.go │ ├── ru.go │ ├── ru_test.go │ ├── weekday.go │ └── weekday_test.go ├── rules.go ├── sort.go └── zh │ ├── after_time.go │ ├── casual_date.go │ ├── casual_date_test.go │ ├── casual_time.go │ ├── exact_month_date.go │ ├── exact_month_test.go │ ├── hour_minute.go │ ├── hour_minute_test.go │ ├── tradition_hour.go │ ├── tradition_hour_test.go │ ├── weedkay.go │ ├── weekday_test.go │ ├── zh.go │ └── zh_test.go ├── wercker.yml └── when.go /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | go-version: [ '1.19', '1.20', '1.21.x' ] 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Setup Go 13 | uses: actions/setup-go@v3 14 | with: 15 | go-version: ${{ matrix.go-version }} 16 | cache: true 17 | - name: Install dependencies 18 | run: go get . 19 | - name: Run tests 20 | run: go test ./... 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @olebedev 2 | /rules/zh/ @RexSkz 3 | /rules/nl @iamreinder 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Oleg Lebedev 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # when [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/olebedev/when) 2 | 3 | > `when` is a natural language date/time parser with pluggable rules and merge strategies 4 | 5 | ### Examples 6 | 7 | - **tonight at 11:10 pm** 8 | - at **Friday afternoon** 9 | - the deadline is **next tuesday 14:00** 10 | - drop me a line **next wednesday at 2:25 p.m** 11 | - it could be done at **11 am past tuesday** 12 | 13 | Check [EN](https://github.com/olebedev/when/blob/master/rules/en) rules and tests of them, for more examples. 14 | 15 | **Needed rule not found?** 16 | Open [an issue](https://github.com/olebedev/when/issues/new) with the case and it will be added asap. 17 | 18 | ### How it works 19 | 20 | Usually, there are several rules added to the parser's instance for checking. Each rule has its own borders - length and offset in provided string. Meanwhile, each rule yields only the first match over the string. So, the library checks all the rules and extracts a cluster of matched rules which have distance between each other less or equal to [`options.Distance`](https://github.com/olebedev/when/blob/master/when.go#L141-L144), which is 5 by default. For example: 21 | 22 | ``` 23 | on next wednesday at 2:25 p.m. 24 | └──────┬─────┘ └───┬───┘ 25 | weekday hour + minute 26 | ``` 27 | 28 | So, we have a cluster of matched rules - `"next wednesday at 2:25 p.m."` in the string representation. 29 | 30 | After that, each rule is applied to the context. In order of definition or in match order, if [`options.MatchByOrder`](https://github.com/olebedev/when/blob/master/when.go#L141-L144) is set to `true`(which it is by default). Each rule could be applied with given merge strategy. By default, it's an [Override](https://github.com/olebedev/when/blob/master/rules/rules.go#L13) strategy. The other strategies are not implemented yet in the rules. **Pull requests are welcome.** 31 | 32 | ### Supported Languages 33 | 34 | - [EN](https://github.com/olebedev/when/blob/master/rules/en) - English 35 | - [RU](https://github.com/olebedev/when/blob/master/rules/ru) - Russian 36 | - [BR](https://github.com/olebedev/when/blob/master/rules/br) - Brazilian Portuguese 37 | - [ZH](https://github.com/olebedev/when/blob/master/rules/zh) - Chinese 38 | - [NL](https://github.com/olebedev/when/blob/master/rules/nl) - Dutch 39 | 40 | ### Install 41 | 42 | The project follows the official [release workflow](https://go.dev/doc/modules/release-workflow). It is recommended to refer to this resource for detailed information on the process. 43 | 44 | To install the latest version: 45 | 46 | ``` 47 | $ go get github.com/olebedev/when@latest 48 | ``` 49 | 50 | ### Usage 51 | 52 | ```go 53 | w := when.New(nil) 54 | w.Add(en.All...) 55 | w.Add(common.All...) 56 | 57 | text := "drop me a line in next wednesday at 2:25 p.m" 58 | r, err := w.Parse(text, time.Now()) 59 | if err != nil { 60 | // an error has occurred 61 | } 62 | if r == nil { 63 | // no matches found 64 | } 65 | 66 | fmt.Println( 67 | "the time", 68 | r.Time.String(), 69 | "mentioned in", 70 | text[r.Index:r.Index+len(r.Text)], 71 | ) 72 | ``` 73 | 74 | #### Distance Option 75 | 76 | ```go 77 | w := when.New(nil) 78 | w.Add(en.All...) 79 | w.Add(common.All...) 80 | 81 | text := "February 23, 2019 | 1:46pm" 82 | 83 | // With default distance (5): 84 | // February 23, 2019 | 1:46pm 85 | // └───┬───┘ 86 | // distance: 9 (1:46pm will be ignored) 87 | 88 | r, _ := w.Parse(text, time.Now()) 89 | fmt.Printf(r.Time.String()) 90 | // "2019-02-23 09:21:21.835182427 -0300 -03" 91 | // 2019-02-23 (correct) 92 | // 09:21:21 ("wrong") 93 | 94 | // With custom distance (10): 95 | w.SetOptions(&rules.Options{ 96 | Distance: 10, 97 | MatchByOrder: true}) 98 | 99 | r, _ = w.Parse(text, time.Now()) 100 | fmt.Printf(r.Time.String()) 101 | // "2019-02-23 13:46:21.559521554 -0300 -03" 102 | // 2019-02-23 (correct) 103 | // 13:46:21 (correct) 104 | ``` 105 | 106 | ### State of the project 107 | 108 | The project is in a more-or-less complete state. It's used for one project already. Bugs will be fixed as soon as they will be found. 109 | 110 | ### TODO 111 | 112 | - [ ] readme: describe all the existing rules 113 | - [ ] implement missed rules for [these examples](https://github.com/mojombo/chronic#examples) 114 | - [ ] add cli and simple rest api server([#2](https://github.com/olebedev/when/issues/2)) 115 | 116 | ### LICENSE 117 | 118 | http://www.apache.org/licenses/LICENSE-2.0 119 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/olebedev/when 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/AlekSi/pointer v1.0.0 7 | github.com/pkg/errors v0.8.1 8 | github.com/stretchr/testify v1.3.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | github.com/stretchr/objx v0.1.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AlekSi/pointer v1.0.0 h1:KWCWzsvFxNLcmM5XmiqHsGTTsuwZMsLFwWF9Y+//bNE= 2 | github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 7 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 12 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 13 | -------------------------------------------------------------------------------- /rules/br/br.go: -------------------------------------------------------------------------------- 1 | package br 2 | 3 | import "github.com/olebedev/when/rules" 4 | 5 | var All = []rules.Rule{ 6 | Weekday(rules.Override), 7 | CasualDate(rules.Override), 8 | CasualTime(rules.Override), 9 | Hour(rules.Override), 10 | HourMinute(rules.Override), 11 | Deadline(rules.Override), 12 | PastTime(rules.Override), 13 | ExactMonthDate(rules.Override), 14 | } 15 | 16 | var WEEKDAY_OFFSET = map[string]int{ 17 | "domingo": 0, 18 | "dom": 0, 19 | "segunda-feira": 1, 20 | "segunda": 1, 21 | "seg": 1, 22 | "terça-feira": 2, 23 | "terça": 2, 24 | "ter": 2, 25 | "quarta-feira": 3, 26 | "quarta": 3, 27 | "qua": 3, 28 | "quinta-feira": 4, 29 | "quinta": 4, 30 | "qui": 4, 31 | "sexta-feira": 5, 32 | "sexta": 5, 33 | "sex": 5, 34 | "sábado": 6, 35 | "sab": 6, 36 | } 37 | 38 | var WEEKDAY_OFFSET_PATTERN = "(?:domingo|dom|segunda-feira|segunda|seg|terça-feira|terça|ter|quarta-feira|quarta|qua|quinta-feira|quinta|qui|sexta-feira|sexta|sex|sábado|sab)" 39 | 40 | var MONTH_OFFSET = map[string]int{ 41 | "janeiro": 1, 42 | "jan.": 1, 43 | "jan": 1, 44 | "fevereiro": 2, 45 | "fev.": 2, 46 | "fev": 2, 47 | "março": 3, 48 | "mar.": 3, 49 | "mar": 3, 50 | "abril": 4, 51 | "abr.": 4, 52 | "abr": 4, 53 | "maio": 5, 54 | "mai.": 5, 55 | "mai": 5, 56 | "junho": 6, 57 | "jun.": 6, 58 | "jun": 6, 59 | "julho": 7, 60 | "jul.": 7, 61 | "jul": 7, 62 | "agosto": 8, 63 | "ago.": 8, 64 | "ago": 8, 65 | "setembro": 9, 66 | "set.": 9, 67 | "set": 9, 68 | "outubro": 10, 69 | "out.": 10, 70 | "out": 10, 71 | "novembro": 11, 72 | "nov.": 11, 73 | "nov": 11, 74 | "dezembro": 12, 75 | "dez.": 12, 76 | "dez": 12, 77 | } 78 | 79 | var MONTH_OFFSET_PATTERN = `(?:janeiro|jan\.?|jan|fevereiro|fev\.?|fev|março|mar\.?|mar|abril|abr\.?|abr|maio|mai\.?|mai|junho|jun\.?|jun|julho|jul\.?|jul|agosto|ago\.?|ago|setembro|set\.?|set|outubro|out\.?|out|novembro|nov\.?|nov|dezembro|dez\.?|dez)` 80 | 81 | var INTEGER_WORDS = map[string]int{ 82 | "uma": 1, 83 | "um": 1, 84 | "duas": 2, 85 | "dois": 2, 86 | "três": 3, 87 | "quatro": 4, 88 | "cinco": 5, 89 | "seis": 6, 90 | "sete": 7, 91 | "oito": 8, 92 | "nove": 9, 93 | "dez": 10, 94 | "onze": 11, 95 | "doze": 12, 96 | } 97 | 98 | var INTEGER_WORDS_PATTERN = `(?:uma|um|duas|dois|três|quatro|cinco|seis|sete|oito|nove|dez|onze|doze)` 99 | 100 | var ORDINAL_WORDS = map[string]int{ 101 | "primeiro": 1, 102 | "1º": 1, 103 | "segunda": 2, 104 | "segundo": 2, 105 | "2ª": 2, 106 | "2º": 2, 107 | "terceira": 3, 108 | "terceiro": 3, 109 | "3ª": 3, 110 | "3º": 3, 111 | "quarta": 4, 112 | "quarto": 4, 113 | "4ª": 4, 114 | "4º": 4, 115 | "quinta": 5, 116 | "quinto": 5, 117 | "5ª": 5, 118 | "5º": 5, 119 | "sexta": 6, 120 | "sexto": 6, 121 | "6ª": 6, 122 | "6º": 6, 123 | "sétima": 7, 124 | "sétimo": 7, 125 | "7ª": 7, 126 | "7º": 7, 127 | "oitava": 8, 128 | "oitavo": 8, 129 | "8ª": 8, 130 | "8º": 8, 131 | "nona": 9, 132 | "nono": 9, 133 | "9ª": 9, 134 | "9º": 9, 135 | "décima": 10, 136 | "décimo": 10, 137 | "10ª": 10, 138 | "10º": 10, 139 | "décima-primeira": 11, 140 | "décima primeira": 11, 141 | "décimo-primeiro": 11, 142 | "décimo primeiro": 11, 143 | "11ª": 11, 144 | "11º": 11, 145 | "décima-segunda": 12, 146 | "décima segunda": 12, 147 | "décimo-segundo": 12, 148 | "décimo segundo": 12, 149 | "12ª": 12, 150 | "12º": 12, 151 | "décima-terceira": 13, 152 | "décima terceira": 13, 153 | "décimo-terceiro": 13, 154 | "décimo terceiro": 13, 155 | "13ª": 13, 156 | "13º": 13, 157 | "décima-quarta": 14, 158 | "décima quarta": 14, 159 | "décimo-quarto": 14, 160 | "décimo quarto": 14, 161 | "14ª": 14, 162 | "14º": 14, 163 | "décima-quinta": 15, 164 | "décima quinta": 15, 165 | "décimo-quinto": 15, 166 | "décimo quinto": 15, 167 | "15ª": 15, 168 | "15º": 15, 169 | "décima-sexta": 16, 170 | "décima sexta": 16, 171 | "décimo-sexto": 16, 172 | "décimo sexto": 16, 173 | "16ª": 16, 174 | "16º": 16, 175 | "décima-sétima": 17, 176 | "décima sétima": 17, 177 | "décimo-sétimo": 17, 178 | "décimo sétimo": 17, 179 | "17ª": 17, 180 | "17º": 17, 181 | "décima-oitava": 18, 182 | "décima oitava": 18, 183 | "décimo-oitavo": 18, 184 | "décimo oitavo": 18, 185 | "18ª": 18, 186 | "18º": 18, 187 | "décima-nona": 19, 188 | "décima nona": 19, 189 | "décimo-nono": 19, 190 | "décimo nono": 19, 191 | "19ª": 19, 192 | "19º": 19, 193 | "vigésima": 20, 194 | "vigésimo": 20, 195 | "20ª": 20, 196 | "20º": 20, 197 | "vigésima-primeira": 21, 198 | "vigésima primeira": 21, 199 | "vigésimo-primeiro": 21, 200 | "vigésimo primeiro": 21, 201 | "21ª": 21, 202 | "21º": 21, 203 | "vigésima-segunda": 22, 204 | "vigésima segunda": 22, 205 | "vigésimo-segundo": 22, 206 | "vigésimo segundo": 22, 207 | "22ª": 22, 208 | "22º": 22, 209 | "vigésima-terceira": 23, 210 | "vigésima terceira": 23, 211 | "vigésimo-terceiro": 23, 212 | "vigésimo terceiro": 23, 213 | "23ª": 23, 214 | "23º": 23, 215 | 216 | "vigésima-quarta": 24, 217 | "vigésima quarta": 24, 218 | "vigésimo-quarto": 24, 219 | "vigésimo quarto": 24, 220 | "24ª": 24, 221 | "24º": 24, 222 | "vigésima-quinta": 25, 223 | "vigésima quinta": 25, 224 | "vigésimo-quinto": 25, 225 | "vigésimo quinto": 25, 226 | "25ª": 25, 227 | "25º": 25, 228 | "vigésima-sexta": 26, 229 | "vigésima sexta": 26, 230 | "vigésimo-sexto": 26, 231 | "vigésimo sexto": 26, 232 | "26ª": 26, 233 | "26º": 26, 234 | "vigésima-sétima": 27, 235 | "vigésima sétima": 27, 236 | "vigésimo-sétimo": 27, 237 | "vigésimo sétimo": 27, 238 | "27ª": 27, 239 | "27º": 27, 240 | "vigésima-oitava": 28, 241 | "vigésima oitava": 28, 242 | "vigésimo-oitavo": 28, 243 | "vigésimo oitavo": 28, 244 | "28ª": 28, 245 | "28º": 28, 246 | "vigésima-nona": 29, 247 | "vigésima nona": 29, 248 | "vigésimo-nono": 29, 249 | "vigésimo nono": 29, 250 | "29ª": 29, 251 | "29º": 29, 252 | "trigésima": 30, 253 | "trigésimo": 30, 254 | "30ª": 30, 255 | "30º": 30, 256 | "trigésima-primeira": 31, 257 | "trigésima primeira": 31, 258 | "trigésimo-primeiro": 31, 259 | "trigésimo primeiro": 31, 260 | "31ª": 31, 261 | "31º": 31, 262 | } 263 | 264 | var ORDINAL_WORDS_PATTERN = `(?:primeir[ao]|1[ªº]|segund[ao]|2[ªº]|terceir[ao]|3[ªº]|quart[ao]|4[ªº]|quint[ao]|5[ªº]|sext[ao]|6[ªº]|sétim[ao]|7[ªº]|oitav[ao]|8[ªº]|non[ao]|9[ªº]|décim[ao]|10[ªº]|décim[ao][- ]primeir[ao]|11[ªº]|décima[ao][- ]segund[ao]|12[ªº]|décim[ao][- ]terceir[ao]|13[ªº]|décim[ao][- ]quart[ao]|14[ªº]|décim[ao][- ]quint[ao]|15[ªº]|décim[ao][- ]sext[ao]|16[ªº]|décim[ao][- ]sétim[ao]|17[ªº]|décim[ao][- ]oitav[ao]|18[ªº]|décim[ao][- ]non[ao]|19[ªº]|vigésim[ao]|20[ªº]|vigésim[ao][- ]primeir[ao]|21[ªº]|vigésim[ao][- ]segund[ao]|22[ªº]|vigésim[ao][- ]terceir[ao]|23[ªº]|vigésim[ao][- ]quart[ao]|24[ªº]|vigésim[ao][- ]quint[ao]|25[ªº]|vigésim[ao][- ]sext[ao]|26[ªº]|vigésim[ao][- ]sétim[ao]|27[ªº]|vigésim[ao][- ]oitav[ao]|28[ªº]|vigésim[ao][- ]non[ao]|29[ªº]|trigésim[ao]|30[ªº]|trigésim[ao][- ]primeir[ao]|31[ªº]|31º)` 265 | -------------------------------------------------------------------------------- /rules/br/br_test.go: -------------------------------------------------------------------------------- 1 | package br_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules/br" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var null = time.Date(2016, time.January, 6, 0, 0, 0, 0, time.UTC) 13 | 14 | type Fixture struct { 15 | Text string 16 | Index int 17 | Phrase string 18 | Diff time.Duration 19 | } 20 | 21 | func ApplyFixtures(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 22 | for i, f := range fixt { 23 | res, err := w.Parse(f.Text, null) 24 | require.Nil(t, err, "[%s] err #%d", name, i) 25 | require.NotNil(t, res, "[%s] res #%d", name, i) 26 | require.Equal(t, f.Index, res.Index, "[%s] index #%d", name, i) 27 | require.Equal(t, f.Phrase, res.Text, "[%s] text #%d", name, i) 28 | require.Equal(t, f.Diff, res.Time.Sub(null), "[%s] diff #%d", name, i) 29 | } 30 | } 31 | 32 | func ApplyFixturesNil(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 33 | for i, f := range fixt { 34 | res, err := w.Parse(f.Text, null) 35 | require.Nil(t, err, "[%s] err #%d", name, i) 36 | require.Nil(t, res, "[%s] res #%d", name, i) 37 | } 38 | } 39 | 40 | func ApplyFixturesErr(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 41 | for i, f := range fixt { 42 | _, err := w.Parse(f.Text, null) 43 | require.NotNil(t, err, "[%s] err #%d", name, i) 44 | require.Equal(t, f.Phrase, err.Error(), "[%s] err text #%d", name, i) 45 | } 46 | } 47 | 48 | func TestAll(t *testing.T) { 49 | w := when.New(nil) 50 | w.Add(br.All...) 51 | 52 | // complex cases 53 | fixt := []Fixture{ 54 | {"hoje de noite às 11:10 pm", 0, "hoje de noite às 11:10 pm", (23 * time.Hour) + (10 * time.Minute)}, 55 | {"na tarde de sexta", 3, "tarde de sexta", ((2 * 24) + 15) * time.Hour}, 56 | {"na próxima terça às 14:00", 3, "próxima terça às 14:00", ((6 * 24) + 14) * time.Hour}, 57 | {"na próxima terça às 2p", 3, "próxima terça às 2p", ((6 * 24) + 14) * time.Hour}, 58 | {"na próxima quarta-feira às 2:25 p.m.", 3, "próxima quarta-feira às 2:25 p.m.", (((7 * 24) + 14) * time.Hour) + (25 * time.Minute)}, 59 | {"11 am última terça", 0, "11 am última terça", -13 * time.Hour}, 60 | } 61 | 62 | ApplyFixtures(t, "br.All...", w, fixt) 63 | } 64 | -------------------------------------------------------------------------------- /rules/br/casual_date.go: -------------------------------------------------------------------------------- 1 | package br 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/AlekSi/pointer" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func CasualDate(s rules.Strategy) rules.Rule { 13 | overwrite := s == rules.Override 14 | 15 | return &rules.F{ 16 | RegExp: regexp.MustCompile("(?i)(?:\\W|^)(agora|hoje|(?:de\\s|nesta\\s|esta\\s)noite|última(?:s|)\\s*noite|(?:amanhã|ontem)\\s*|amanhã|ontem)(?:\\W|$)"), 17 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 18 | lower := strings.ToLower(strings.TrimSpace(m.String())) 19 | 20 | switch { 21 | case regexContains("(nesta|esta|hoje)(\\s|\\s([aà]|de)\\s)noite", lower): 22 | if c.Hour == nil && c.Minute == nil || overwrite { 23 | c.Hour = pointer.ToInt(23) 24 | c.Minute = pointer.ToInt(0) 25 | } 26 | case strings.Contains(lower, "hoje"): 27 | // c.Hour = pointer.ToInt(18) 28 | case strings.Contains(lower, "amanhã"): 29 | if c.Duration == 0 || overwrite { 30 | c.Duration += time.Hour * 24 31 | } 32 | case strings.Contains(lower, "ontem"): 33 | if c.Duration == 0 || overwrite { 34 | c.Duration -= time.Hour * 24 35 | } 36 | case regexContains("(ontem|última)(\\s|\\s([aà]|de)\\s)noite", lower): 37 | if (c.Hour == nil && c.Duration == 0) || overwrite { 38 | c.Hour = pointer.ToInt(23) 39 | c.Duration -= time.Hour * 24 40 | } 41 | } 42 | 43 | return true, nil 44 | }, 45 | } 46 | } 47 | 48 | func regexContains(regex string, text string) bool { 49 | contains, _ := regexp.MatchString(regex, text) 50 | 51 | return contains 52 | } 53 | -------------------------------------------------------------------------------- /rules/br/casual_test.go: -------------------------------------------------------------------------------- 1 | package br_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/br" 10 | ) 11 | 12 | func TestCasualDate(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"O prazo final é agora, ok", 17, "agora", 0}, 15 | {"O prazo final é hoje", 17, "hoje", 0}, 16 | {"O prazo final é esta noite", 17, "esta noite", 23 * time.Hour}, 17 | {"O prazo final é amanhã à noite", 17, "amanhã ", time.Hour * 24}, 18 | {"O prazo foi ontem à noite", 12, "ontem ", -(time.Hour * 24)}, 19 | } 20 | 21 | w := when.New(nil) 22 | w.Add(br.CasualDate(rules.Skip)) 23 | 24 | ApplyFixtures(t, "br.CasualDate", w, fixt) 25 | } 26 | 27 | func TestCasualTime(t *testing.T) { 28 | fixt := []Fixture{ 29 | {"O prazo foi esta manhã ", 12, "esta manhã", 8 * time.Hour}, 30 | {"O prazo final foi ao meio-dia ", 18, "ao meio-dia", 12 * time.Hour}, 31 | {"O prazo final foi esta tarde ", 18, "esta tarde", 15 * time.Hour}, 32 | {"O prazo foi nesta noite ", 12, "nesta noite", 18 * time.Hour}, 33 | } 34 | 35 | w := when.New(nil) 36 | w.Add(br.CasualTime(rules.Skip)) 37 | 38 | ApplyFixtures(t, "br.CasualTime", w, fixt) 39 | } 40 | 41 | func TestCasualDateCasualTime(t *testing.T) { 42 | fixt := []Fixture{ 43 | {"O prazo final é amanhã de tarde", 17, "amanhã de tarde", (15 + 24) * time.Hour}, 44 | } 45 | 46 | w := when.New(nil) 47 | w.Add( 48 | br.CasualDate(rules.Skip), 49 | br.CasualTime(rules.Override), 50 | ) 51 | 52 | ApplyFixtures(t, "br.CasualDate|br.CasualTime", w, fixt) 53 | } 54 | -------------------------------------------------------------------------------- /rules/br/casual_time.go: -------------------------------------------------------------------------------- 1 | package br 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/AlekSi/pointer" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func CasualTime(s rules.Strategy) rules.Rule { 13 | overwrite := s == rules.Override 14 | 15 | return &rules.F{ 16 | RegExp: regexp.MustCompile(`(?i)(?:\W|^)(((?:nesta|esta|ao|à))?\s*(manhã|tarde|noite|meio[- ]dia))`), 17 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 18 | lower := strings.ToLower(strings.TrimSpace(m.String())) 19 | 20 | if (c.Hour != nil || c.Minute != nil) && !overwrite { 21 | return false, nil 22 | } 23 | 24 | switch { 25 | case strings.Contains(lower, "tarde"): 26 | if o.Afternoon != 0 { 27 | c.Hour = &o.Afternoon 28 | } else { 29 | c.Hour = pointer.ToInt(15) 30 | } 31 | c.Minute = pointer.ToInt(0) 32 | case strings.Contains(lower, "noite"): 33 | if o.Evening != 0 { 34 | c.Hour = &o.Evening 35 | } else { 36 | c.Hour = pointer.ToInt(18) 37 | } 38 | c.Minute = pointer.ToInt(0) 39 | case strings.Contains(lower, "manhã"): 40 | if o.Morning != 0 { 41 | c.Hour = &o.Morning 42 | } else { 43 | c.Hour = pointer.ToInt(8) 44 | } 45 | c.Minute = pointer.ToInt(0) 46 | case strings.Contains(lower, "meio-dia"), strings.Contains(lower, "meio dia"): 47 | if o.Noon != 0 { 48 | c.Hour = &o.Noon 49 | } else { 50 | c.Hour = pointer.ToInt(12) 51 | } 52 | c.Minute = pointer.ToInt(0) 53 | } 54 | 55 | return true, nil 56 | }, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /rules/br/deadline.go: -------------------------------------------------------------------------------- 1 | package br 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/AlekSi/pointer" 10 | "github.com/olebedev/when/rules" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func Deadline(s rules.Strategy) rules.Rule { 15 | overwrite := s == rules.Override 16 | 17 | return &rules.F{ 18 | RegExp: regexp.MustCompile( 19 | "(?i)(?:\\W|^)(dentro\\sde|em)\\s*" + 20 | "(?:(" + INTEGER_WORDS_PATTERN + "|[0-9]+|(?:\\s*pouc[oa](?:s|)?|algu(?:mas|m|ns)?|mei[oa]?))\\s*" + 21 | "(segundos?|min(?:uto)?s?|horas?|dias?|semanas?|mês|meses|anos?)\\s*)" + 22 | "(?:\\W|$)"), 23 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 24 | numStr := strings.TrimSpace(m.Captures[1]) 25 | 26 | var num int 27 | var err error 28 | 29 | if n, ok := INTEGER_WORDS[numStr]; ok { 30 | num = n 31 | } else if strings.Contains(numStr, "pouc") || strings.Contains(numStr, "algu") { 32 | num = 3 33 | } else if strings.Contains(numStr, "mei") { 34 | // pass 35 | } else { 36 | num, err = strconv.Atoi(numStr) 37 | if err != nil { 38 | return false, errors.Wrapf(err, "convert '%s' to int", numStr) 39 | } 40 | } 41 | 42 | exponent := strings.TrimSpace(m.Captures[2]) 43 | 44 | if !strings.Contains(numStr, "mei") { 45 | switch { 46 | case strings.Contains(exponent, "segundo"): 47 | if c.Duration == 0 || overwrite { 48 | c.Duration = time.Duration(num) * time.Second 49 | } 50 | case strings.Contains(exponent, "min"): 51 | if c.Duration == 0 || overwrite { 52 | c.Duration = time.Duration(num) * time.Minute 53 | } 54 | case strings.Contains(exponent, "hora"): 55 | if c.Duration == 0 || overwrite { 56 | c.Duration = time.Duration(num) * time.Hour 57 | } 58 | case strings.Contains(exponent, "dia"): 59 | if c.Duration == 0 || overwrite { 60 | c.Duration = time.Duration(num) * 24 * time.Hour 61 | } 62 | case strings.Contains(exponent, "semana"): 63 | if c.Duration == 0 || overwrite { 64 | c.Duration = time.Duration(num) * 7 * 24 * time.Hour 65 | } 66 | case strings.Contains(exponent, "mês"), strings.Contains(exponent, "meses"): 67 | if c.Month == nil || overwrite { 68 | c.Month = pointer.ToInt((int(ref.Month()) + num) % 12) 69 | } 70 | case strings.Contains(exponent, "ano"): 71 | if c.Year == nil || overwrite { 72 | c.Year = pointer.ToInt(ref.Year() + num) 73 | } 74 | } 75 | } else { 76 | switch { 77 | case strings.Contains(exponent, "hora"): 78 | if c.Duration == 0 || overwrite { 79 | c.Duration = 30 * time.Minute 80 | } 81 | case strings.Contains(exponent, "dia"): 82 | if c.Duration == 0 || overwrite { 83 | c.Duration = 12 * time.Hour 84 | } 85 | case strings.Contains(exponent, "semana"): 86 | if c.Duration == 0 || overwrite { 87 | c.Duration = 7 * 12 * time.Hour 88 | } 89 | case strings.Contains(exponent, "mês"), strings.Contains(exponent, "meses"): 90 | if c.Duration == 0 || overwrite { 91 | // 2 weeks 92 | c.Duration = 14 * 24 * time.Hour 93 | } 94 | case strings.Contains(exponent, "ano"): 95 | if c.Month == nil || overwrite { 96 | c.Month = pointer.ToInt((int(ref.Month()) + 6) % 12) 97 | } 98 | } 99 | } 100 | 101 | return true, nil 102 | }, 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /rules/br/deadline_test.go: -------------------------------------------------------------------------------- 1 | package br_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/br" 10 | ) 11 | 12 | func TestDeadline(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"dentro de meia hora", 0, "dentro de meia hora", time.Hour / 2}, 15 | {"dentro de 1 hora", 0, "dentro de 1 hora", time.Hour}, 16 | {"em 5 minutos", 0, "em 5 minutos", time.Minute * 5}, 17 | {"Em 5 minutos eu irei para casa", 0, "Em 5 minutos", time.Minute * 5}, 18 | {"nós precisamos fazer algo dentro de 10 dias.", 27, "dentro de 10 dias", 10 * 24 * time.Hour}, 19 | {"nós temos que fazer algo em cinco dias.", 26, "em cinco dias", 5 * 24 * time.Hour}, 20 | {"nós temos que fazer algo em 5 dias.", 26, "em 5 dias", 5 * 24 * time.Hour}, 21 | {"Em 5 segundos, um carro precisa se mover", 0, "Em 5 segundos", 5 * time.Second}, 22 | {"dentro de duas semanas", 0, "dentro de duas semanas", 14 * 24 * time.Hour}, 23 | {"dentro de um mês", 0, "dentro de um mês", 31 * 24 * time.Hour}, 24 | {"dentro de alguns meses", 0, "dentro de alguns meses", 91 * 24 * time.Hour}, 25 | {"dentro de poucos meses", 0, "dentro de poucos meses", 91 * 24 * time.Hour}, 26 | {"dentro de um ano", 0, "dentro de um ano", 366 * 24 * time.Hour}, 27 | {"em uma semana", 0, "em uma semana", 7 * 24 * time.Hour}, 28 | } 29 | 30 | w := when.New(nil) 31 | w.Add(br.Deadline(rules.Skip)) 32 | 33 | ApplyFixtures(t, "br.Deadline", w, fixt) 34 | } 35 | -------------------------------------------------------------------------------- /rules/br/exact_month_date.go: -------------------------------------------------------------------------------- 1 | package br 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func ExactMonthDate(s rules.Strategy) rules.Rule { 13 | overwrite := s == rules.Override 14 | 15 | return &rules.F{ 16 | RegExp: regexp.MustCompile("(?i)" + 17 | "(?:\\W|^)" + 18 | "(?:(?:(\\d{1,2})|(" + ORDINAL_WORDS_PATTERN[3:] + // skip '(?:' 19 | ")(?:\\sdia\\sde\\s|\\sde\\s|\\s))*" + 20 | "(" + MONTH_OFFSET_PATTERN[3:] + // skip '(?:' 21 | "(?:\\W|$)", 22 | ), 23 | 24 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 25 | _ = overwrite 26 | 27 | num := strings.ToLower(strings.TrimSpace(m.Captures[0])) 28 | ord := strings.ToLower(strings.TrimSpace(m.Captures[1])) 29 | mon := strings.ToLower(strings.TrimSpace(m.Captures[2])) 30 | 31 | monInt, ok := MONTH_OFFSET[mon] 32 | if !ok { 33 | return false, nil 34 | } 35 | 36 | c.Month = &monInt 37 | 38 | if ord != "" { 39 | ordInt, ok := ORDINAL_WORDS[ord] 40 | if !ok { 41 | return false, nil 42 | } 43 | 44 | c.Day = &ordInt 45 | } 46 | 47 | if num != "" { 48 | n, err := strconv.ParseInt(num, 10, 8) 49 | if err != nil { 50 | return false, nil 51 | } 52 | 53 | num := int(n) 54 | 55 | c.Day = &num 56 | } 57 | 58 | return true, nil 59 | }, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rules/br/exact_month_date_test.go: -------------------------------------------------------------------------------- 1 | package br_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/br" 10 | ) 11 | 12 | func TestExactMonthDate(t *testing.T) { 13 | w := when.New(nil) 14 | w.Add(br.ExactMonthDate(rules.Override)) 15 | 16 | fixtok := []Fixture{ 17 | {"3 de março", 0, "3 de março", 1368 * time.Hour}, 18 | {"1 de setembro", 0, "1 de setembro", 5736 * time.Hour}, 19 | {"1 set", 0, "1 set", 5736 * time.Hour}, 20 | {"1 set.", 0, "1 set.", 5736 * time.Hour}, 21 | {"1º de setembro", 0, "1º de setembro", 5736 * time.Hour}, 22 | {"1º set.", 0, "1º set.", 5736 * time.Hour}, 23 | {"7 de março", 0, "7 de março", 1464 * time.Hour}, 24 | {"21 de outubro", 0, "21 de outubro", 6936 * time.Hour}, 25 | {"vigésimo dia de dezembro", 0, "vigésimo dia de dezembro", 8376 * time.Hour}, 26 | {"10º dia de março", 0, "10º dia de março", 1536 * time.Hour}, 27 | {"4 jan.", 0, "4 jan.", -48 * time.Hour}, 28 | {"fevereiro", 0, "fevereiro", 744 * time.Hour}, 29 | {"outubro", 0, "outubro", 6576 * time.Hour}, 30 | {"jul.", 0, "jul.", 4368 * time.Hour}, 31 | {"junho", 0, "junho", 3648 * time.Hour}, 32 | } 33 | 34 | ApplyFixtures(t, "br.ExactMonthDate", w, fixtok) 35 | } 36 | -------------------------------------------------------------------------------- /rules/br/hour.go: -------------------------------------------------------------------------------- 1 | package br 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | 10 | "github.com/olebedev/when/rules" 11 | ) 12 | 13 | /* 14 | "5pm" 15 | "5 pm" 16 | "5am" 17 | "5pm" 18 | "5A." 19 | "5P." 20 | "11 P.M." 21 | https://play.golang.org/p/2Gh35Sl3KP 22 | */ 23 | 24 | func Hour(s rules.Strategy) rules.Rule { 25 | 26 | return &rules.F{ 27 | RegExp: regexp.MustCompile("(?i)(?:\\W|^)" + 28 | "(\\d{1,2})" + 29 | "(?:\\s*(A\\.|P\\.|A\\.M\\.|P\\.M\\.|AM?|PM?))" + 30 | "(?:\\W|$)"), 31 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 32 | if c.Hour != nil && s != rules.Override { 33 | return false, nil 34 | } 35 | 36 | hour, err := strconv.Atoi(m.Captures[0]) 37 | if err != nil { 38 | return false, errors.Wrap(err, "hour rule") 39 | } 40 | 41 | if hour > 12 { 42 | return false, nil 43 | } 44 | 45 | zero := 0 46 | switch m.Captures[1][0] { 47 | case 65, 97: // am 48 | c.Hour = &hour 49 | case 80, 112: // pm 50 | if hour < 12 { 51 | hour += 12 52 | } 53 | c.Hour = &hour 54 | } 55 | 56 | c.Minute = &zero 57 | return true, nil 58 | }, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /rules/br/hour_minute.go: -------------------------------------------------------------------------------- 1 | package br 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | /* 13 | {"5:30pm", 0, "5:30pm", 0}, 14 | {"5:30 pm", 0, "5:30 pm", 0}, 15 | {"7-10pm", 0, "7-10pm", 0}, 16 | {"5-30", 0, "5-30", 0}, 17 | {"05:30pm", 0, "05:30pm", 0}, 18 | {"05:30 pm", 0, "05:30 pm", 0}, 19 | {"05:30", 0, "05:30", 0}, 20 | {"05-30", 0, "05-30", 0}, 21 | {"7-10 pm", 0, "7-10 pm", 0}, 22 | {"11.10 pm", 0, "11.10 pm", 0}, 23 | 24 | https://play.golang.org/p/hXl7C8MWNr 25 | */ 26 | 27 | // 1. - int 28 | // 2. - int 29 | // 3. - ext? 30 | 31 | func HourMinute(s rules.Strategy) rules.Rule { 32 | return &rules.F{ 33 | RegExp: regexp.MustCompile("(?i)(?:\\W|^)" + 34 | "((?:[0-1]{0,1}[0-9])|(?:2[0-3]))" + 35 | "(?:\\:|:|\\-|h)" + 36 | "((?:[0-5][0-9]))m*" + 37 | "(?:\\s*(A\\.|P\\.|A\\.M\\.|P\\.M\\.|AM?|PM?))?" + 38 | "(?:\\W|$)"), 39 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 40 | if (c.Hour != nil || c.Minute != nil) && s != rules.Override { 41 | return false, nil 42 | } 43 | 44 | hour, err := strconv.Atoi(m.Captures[0]) 45 | if err != nil { 46 | return false, errors.Wrap(err, "hour minute rule") 47 | } 48 | 49 | minutes, err := strconv.Atoi(m.Captures[1]) 50 | if err != nil { 51 | return false, errors.Wrap(err, "hour minute rule") 52 | } 53 | 54 | if minutes > 59 { 55 | return false, nil 56 | } 57 | c.Minute = &minutes 58 | 59 | if m.Captures[2] != "" { 60 | if hour > 12 { 61 | return false, nil 62 | } 63 | switch m.Captures[2][0] { 64 | case 65, 97: // am 65 | c.Hour = &hour 66 | case 80, 112: // pm 67 | if hour < 12 { 68 | hour += 12 69 | } 70 | c.Hour = &hour 71 | } 72 | } else { 73 | if hour > 23 { 74 | return false, nil 75 | } 76 | c.Hour = &hour 77 | } 78 | 79 | return true, nil 80 | }, 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /rules/br/hour_minute_test.go: -------------------------------------------------------------------------------- 1 | package br_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/br" 10 | ) 11 | 12 | func TestHourMinute(t *testing.T) { 13 | w := when.New(nil) 14 | w.Add(br.HourMinute(rules.Override)) 15 | 16 | fixtok := []Fixture{ 17 | {"5:30pm", 0, "5:30pm", (17 * time.Hour) + (30 * time.Minute)}, 18 | {"at 5:30 pm", 3, "5:30 pm", (17 * time.Hour) + (30 * time.Minute)}, 19 | {"at 5:59 pm", 3, "5:59 pm", (17 * time.Hour) + (59 * time.Minute)}, 20 | {"at 5-59 pm", 3, "5-59 pm", (17 * time.Hour) + (59 * time.Minute)}, 21 | {"at 17-59 pam", 3, "17-59", (17 * time.Hour) + (59 * time.Minute)}, 22 | {"up to 11:10 pm", 6, "11:10 pm", (23 * time.Hour) + (10 * time.Minute)}, 23 | {"19h35m", 0, "19h35", (19 * time.Hour) + (35 * time.Minute)}, 24 | } 25 | 26 | fixtnil := []Fixture{ 27 | {"28:30pm", 0, "", 0}, 28 | {"12:61pm", 0, "", 0}, 29 | {"24:10", 0, "", 0}, 30 | } 31 | 32 | ApplyFixtures(t, "br.HourMinute", w, fixtok) 33 | ApplyFixturesNil(t, "on.HourMinute nil", w, fixtnil) 34 | 35 | w.Add(br.Hour(rules.Skip)) 36 | ApplyFixtures(t, "br.HourMinute|br.Hour", w, fixtok) 37 | ApplyFixturesNil(t, "on.HourMinute|br.Hour nil", w, fixtnil) 38 | 39 | w = when.New(nil) 40 | w.Add( 41 | br.Hour(rules.Override), 42 | br.HourMinute(rules.Override), 43 | ) 44 | 45 | ApplyFixtures(t, "br.Hour|br.HourMinute", w, fixtok) 46 | ApplyFixturesNil(t, "on.Hour|br.HourMinute nil", w, fixtnil) 47 | } 48 | -------------------------------------------------------------------------------- /rules/br/hour_test.go: -------------------------------------------------------------------------------- 1 | package br_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/br" 10 | ) 11 | 12 | func TestHour(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"5pm", 0, "5pm", 17 * time.Hour}, 15 | {"at 5 pm", 3, "5 pm", 17 * time.Hour}, 16 | {"at 5 P.", 3, "5 P.", 17 * time.Hour}, 17 | {"at 12 P.", 3, "12 P.", 12 * time.Hour}, 18 | {"at 1 P.", 3, "1 P.", 13 * time.Hour}, 19 | {"at 5 am", 3, "5 am", 5 * time.Hour}, 20 | {"at 5A", 3, "5A", 5 * time.Hour}, 21 | {"at 5A.", 3, "5A.", 5 * time.Hour}, 22 | {"5A.", 0, "5A.", 5 * time.Hour}, 23 | {"11 P.M.", 0, "11 P.M.", 23 * time.Hour}, 24 | } 25 | 26 | w := when.New(nil) 27 | w.Add(br.Hour(rules.Override)) 28 | 29 | ApplyFixtures(t, "br.Hour", w, fixt) 30 | } 31 | -------------------------------------------------------------------------------- /rules/br/past_time.go: -------------------------------------------------------------------------------- 1 | package br 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/AlekSi/pointer" 10 | "github.com/olebedev/when/rules" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func PastTime(s rules.Strategy) rules.Rule { 15 | overwrite := s == rules.Override 16 | 17 | return &rules.F{ 18 | RegExp: regexp.MustCompile( 19 | "(?i)(?:\\W|^)\\s*" + 20 | "(" + INTEGER_WORDS_PATTERN + "|[0-9]+|umas|uma|um|uns|pouc[ao]s*|algu(?:ns|m)|mei[oa]?)\\s*" + 21 | "(segundos?|min(?:uto)?s?|hora?s?|dia?s?|semana?s?|mês?|meses?|ano?s?)(\\satrás)\\s*" + 22 | "(?:\\W|$)|" + 23 | "(?i)(?:há)\\s*" + 24 | "(" + INTEGER_WORDS_PATTERN + "|[0-9]+|umas|uma|um|uns|pouc[ao]s*|algu(?:ns|m)|mei[oa]?)\\s*" + 25 | "(segundos?|min(?:uto)?s?|hora?s?|dia?s?|semana?s?|mês?|meses?|ano?s?)(\\satrás)*\\s*" + 26 | "(?:\\W|$)"), 27 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 28 | var start_index_for_captures int 29 | 30 | if strings.TrimSpace(m.Captures[0]) == "" { 31 | start_index_for_captures = 3 32 | } 33 | 34 | numStr := strings.TrimSpace( 35 | m.Captures[start_index_for_captures+0]) 36 | 37 | var num int 38 | var err error 39 | 40 | if n, ok := INTEGER_WORDS[numStr]; ok { 41 | num = n 42 | } else if numStr == "um" || numStr == "uma" { 43 | num = 1 44 | } else if numStr == "umas" || numStr == "uns" || strings.Contains(numStr, "pouc") || strings.Contains(numStr, "algu") { 45 | num = 3 46 | } else if strings.Contains(numStr, "mei") { 47 | // pass 48 | } else { 49 | num, err = strconv.Atoi(numStr) 50 | if err != nil { 51 | return false, errors.Wrapf(err, "convert '%s' to int", numStr) 52 | } 53 | } 54 | 55 | exponent := strings.TrimSpace( 56 | m.Captures[start_index_for_captures+1]) 57 | 58 | if !strings.Contains(numStr, "mei") { 59 | switch { 60 | case strings.Contains(exponent, "segund"): 61 | if c.Duration == 0 || overwrite { 62 | c.Duration = -(time.Duration(num) * time.Second) 63 | } 64 | case strings.Contains(exponent, "min"): 65 | if c.Duration == 0 || overwrite { 66 | c.Duration = -(time.Duration(num) * time.Minute) 67 | } 68 | case strings.Contains(exponent, "hora"): 69 | if c.Duration == 0 || overwrite { 70 | c.Duration = -(time.Duration(num) * time.Hour) 71 | } 72 | case strings.Contains(exponent, "dia"): 73 | if c.Duration == 0 || overwrite { 74 | c.Duration = -(time.Duration(num) * 24 * time.Hour) 75 | } 76 | case strings.Contains(exponent, "semana"): 77 | if c.Duration == 0 || overwrite { 78 | c.Duration = -(time.Duration(num) * 7 * 24 * time.Hour) 79 | } 80 | case strings.Contains(exponent, "mês"), strings.Contains(exponent, "meses"): 81 | if c.Month == nil || overwrite { 82 | c.Month = pointer.ToInt((int(ref.Month()) - num) % 12) 83 | } 84 | case strings.Contains(exponent, "ano"): 85 | if c.Year == nil || overwrite { 86 | c.Year = pointer.ToInt(ref.Year() - num) 87 | } 88 | } 89 | } else { 90 | switch { 91 | case strings.Contains(exponent, "hora"): 92 | if c.Duration == 0 || overwrite { 93 | c.Duration = -(30 * time.Minute) 94 | } 95 | case strings.Contains(exponent, "dia"): 96 | if c.Duration == 0 || overwrite { 97 | c.Duration = -(12 * time.Hour) 98 | } 99 | case strings.Contains(exponent, "semanas"): 100 | if c.Duration == 0 || overwrite { 101 | c.Duration = -(7 * 12 * time.Hour) 102 | } 103 | case strings.Contains(exponent, "mês"), strings.Contains(exponent, "meses"): 104 | if c.Duration == 0 || overwrite { 105 | // 2 weeks 106 | c.Duration = -(14 * 24 * time.Hour) 107 | } 108 | case strings.Contains(exponent, "ano"): 109 | if c.Month == nil || overwrite { 110 | c.Month = pointer.ToInt((int(ref.Month()) - 6) % 12) 111 | } 112 | } 113 | } 114 | 115 | return true, nil 116 | }, 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /rules/br/past_time_test.go: -------------------------------------------------------------------------------- 1 | package br_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/br" 10 | ) 11 | 12 | func TestPastTime(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"meia hora atrás", 0, "meia hora atrás", -(time.Hour / 2)}, 15 | {"1 hora atrás", 0, "1 hora atrás", -(time.Hour)}, 16 | {"5 minutos atrás", 0, "5 minutos atrás", -(time.Minute * 5)}, 17 | {"5 minutos atrás eu fui ao zoológico", 0, "5 minutos atrás", -(time.Minute * 5)}, 18 | {"nós fizemos algo 10 dias atrás.", 18, "10 dias atrás", -(10 * 24 * time.Hour)}, 19 | {"nós fizemos algo cinco dias atrás.", 18, "cinco dias atrás", -(5 * 24 * time.Hour)}, 20 | {"fizemos algo 5 dias atrás.", 13, "5 dias atrás", -(5 * 24 * time.Hour)}, 21 | {"5 segundos atrás, um carro foi movido", 0, "5 segundos atrás", -(5 * time.Second)}, 22 | {"duas semanas atrás", 0, "duas semanas atrás", -(14 * 24 * time.Hour)}, 23 | {"um mês atrás", 0, "um mês atrás", -(31 * 24 * time.Hour)}, 24 | {"uns meses atrás", 0, "uns meses atrás", -(92 * 24 * time.Hour)}, 25 | {"há um ano", 4, "um ano", -(365 * 24 * time.Hour)}, 26 | {"há duas semanas", 4, "duas semanas", -(2 * 7 * 24 * time.Hour)}, 27 | {"poucas semanas atrás", 0, "poucas semanas atrás", -(3 * 7 * 24 * time.Hour)}, 28 | {"há poucas semanas", 4, "poucas semanas", -(3 * 7 * 24 * time.Hour)}, 29 | {"alguns dias atrás", 0, "alguns dias atrás", -(3 * 24 * time.Hour)}, 30 | {"há alguns dias", 4, "alguns dias", -(3 * 24 * time.Hour)}, 31 | } 32 | 33 | w := when.New(nil) 34 | w.Add(br.PastTime(rules.Skip)) 35 | 36 | ApplyFixtures(t, "br.PastTime", w, fixt) 37 | } 38 | -------------------------------------------------------------------------------- /rules/br/weekday.go: -------------------------------------------------------------------------------- 1 | package br 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | ) 10 | 11 | func Weekday(s rules.Strategy) rules.Rule { 12 | overwrite := s == rules.Override 13 | 14 | return &rules.F{ 15 | RegExp: regexp.MustCompile("(?i)" + 16 | "(?:\\W|^)" + 17 | "(?:n[ao]\\s*?)?" + 18 | "(?:(nest[ae]|ess[ae]|últim[a|o]|próxim[ao])\\s*)?" + 19 | "(" + WEEKDAY_OFFSET_PATTERN[3:] + // skip '(?:' 20 | "(?:\\s*(passad[ao]|que\\svem))?" + 21 | "(?:\\W|$)", 22 | ), 23 | 24 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 25 | _ = overwrite 26 | 27 | day := strings.ToLower(strings.TrimSpace(m.Captures[1])) 28 | norm := strings.ToLower(strings.TrimSpace(m.Captures[0] + m.Captures[2])) 29 | 30 | if norm == "" { 31 | norm = "próxim[ao]" 32 | } 33 | dayInt, ok := WEEKDAY_OFFSET[day] 34 | if !ok { 35 | return false, nil 36 | } 37 | 38 | if c.Duration != 0 && !overwrite { 39 | return false, nil 40 | } 41 | 42 | // Switch: 43 | switch { 44 | case strings.Contains(norm, "passad") || strings.Contains(norm, "últim"): 45 | diff := int(ref.Weekday()) - dayInt 46 | if diff > 0 { 47 | c.Duration = -time.Duration(diff*24) * time.Hour 48 | } else if diff < 0 { 49 | c.Duration = -time.Duration(7+diff) * 24 * time.Hour 50 | } else { 51 | c.Duration = -(7 * 24 * time.Hour) 52 | } 53 | case strings.Contains(norm, "próxim"), strings.Contains(norm, "que vem"): 54 | diff := dayInt - int(ref.Weekday()) 55 | if diff > 0 { 56 | c.Duration = time.Duration(diff*24) * time.Hour 57 | } else if diff < 0 { 58 | c.Duration = time.Duration(7+diff) * 24 * time.Hour 59 | } else { 60 | c.Duration = 7 * 24 * time.Hour 61 | } 62 | case strings.Contains(norm, "nest"), strings.Contains(norm, "ess"): 63 | if int(ref.Weekday()) < dayInt { 64 | diff := dayInt - int(ref.Weekday()) 65 | if diff > 0 { 66 | c.Duration = time.Duration(diff*24) * time.Hour 67 | } else if diff < 0 { 68 | c.Duration = time.Duration(7+diff) * 24 * time.Hour 69 | } else { 70 | c.Duration = 7 * 24 * time.Hour 71 | } 72 | } else if int(ref.Weekday()) > dayInt { 73 | diff := int(ref.Weekday()) - dayInt 74 | if diff > 0 { 75 | c.Duration = -time.Duration(diff*24) * time.Hour 76 | } else if diff < 0 { 77 | c.Duration = -time.Duration(7+diff) * 24 * time.Hour 78 | } else { 79 | c.Duration = -(7 * 24 * time.Hour) 80 | } 81 | } 82 | } 83 | 84 | return true, nil 85 | }, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /rules/br/weekday_test.go: -------------------------------------------------------------------------------- 1 | package br_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/br" 10 | ) 11 | 12 | func TestWeekday(t *testing.T) { 13 | // current is Friday 14 | fixt := []Fixture{ 15 | // past/last 16 | {"faça isto para a Segunda passada", 18, "Segunda passada", -(2 * 24 * time.Hour)}, 17 | {"sábado passado", 0, "sábado passado", -(4 * 24 * time.Hour)}, 18 | {"sexta-feira passada", 0, "sexta-feira passada", -(5 * 24 * time.Hour)}, 19 | {"quarta-feira passada", 0, "quarta-feira passada", -(7 * 24 * time.Hour)}, 20 | {"terça passada", 0, "terça passada", -(24 * time.Hour)}, 21 | // // next 22 | {"na próxima terça-feira", 3, "próxima terça-feira", 6 * 24 * time.Hour}, 23 | {"me ligue na próxima quarta", 12, "próxima quarta", 7 * 24 * time.Hour}, 24 | {"sábado que vem", 0, "sábado que vem", 3 * 24 * time.Hour}, 25 | // // this 26 | {"essa terça-feira", 0, "essa terça-feira", -(24 * time.Hour)}, 27 | {"liga pra mim nesta quarta", 13, "nesta quarta", 0}, 28 | {"neste sábado", 0, "neste sábado", 3 * 24 * time.Hour}, 29 | } 30 | 31 | w := when.New(nil) 32 | 33 | w.Add(br.Weekday(rules.Override)) 34 | 35 | ApplyFixtures(t, "br.Weekday", w, fixt) 36 | } 37 | -------------------------------------------------------------------------------- /rules/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/olebedev/when/rules" 4 | 5 | var All = []rules.Rule{ 6 | SlashDMY(rules.Override), 7 | } 8 | -------------------------------------------------------------------------------- /rules/common/common_test.go: -------------------------------------------------------------------------------- 1 | package common_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules/common" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var null = time.Date(2016, time.July, 15, 0, 0, 0, 0, time.UTC) 13 | 14 | // July 15 days offset from the begining of the year 15 | const OFFSET = 197 16 | 17 | type Fixture struct { 18 | Text string 19 | Index int 20 | Phrase string 21 | Diff time.Duration 22 | } 23 | 24 | func ApplyFixtures(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 25 | for i, f := range fixt { 26 | res, err := w.Parse(f.Text, null) 27 | require.Nil(t, err, "[%s] err #%d", name, i) 28 | require.NotNil(t, res, "[%s] res #%d", name, i) 29 | require.Equal(t, f.Index, res.Index, "[%s] index #%d", name, i) 30 | require.Equal(t, f.Phrase, res.Text, "[%s] text #%d", name, i) 31 | require.Equal(t, f.Diff, res.Time.Sub(null), "[%s] diff #%d", name, i) 32 | } 33 | } 34 | 35 | func ApplyFixturesNil(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 36 | for i, f := range fixt { 37 | res, err := w.Parse(f.Text, null) 38 | require.Nil(t, err, "[%s] err #%d", name, i) 39 | require.Nil(t, res, "[%s] res #%d", name, i) 40 | } 41 | } 42 | 43 | func ApplyFixturesErr(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 44 | for i, f := range fixt { 45 | _, err := w.Parse(f.Text, null) 46 | require.NotNil(t, err, "[%s] err #%d", name, i) 47 | require.Equal(t, f.Phrase, err.Error(), "[%s] err text #%d", name, i) 48 | } 49 | } 50 | 51 | func TestAll(t *testing.T) { 52 | w := when.New(nil) 53 | w.Add(common.All...) 54 | 55 | // complex cases 56 | fixt := []Fixture{} 57 | ApplyFixtures(t, "common.All...", w, fixt) 58 | } 59 | -------------------------------------------------------------------------------- /rules/common/slash_dmy.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | ) 10 | 11 | /* 12 | 13 | - DD/MM/YYYY 14 | - 11/3/2015 15 | - 11/3/2015 16 | - 11/3 17 | 18 | also with "\", gift for windows' users 19 | 20 | https://play.golang.org/p/29LkTfe1Xr 21 | */ 22 | 23 | var MONTHS_DAYS = []int{ 24 | 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 25 | } 26 | 27 | func getDays(year, month int) int { 28 | // naive leap year check 29 | if (year-2000)%4 == 0 && month == 2 { 30 | return 29 31 | } 32 | return MONTHS_DAYS[month] 33 | } 34 | 35 | func SlashDMY(s rules.Strategy) rules.Rule { 36 | 37 | return &rules.F{ 38 | RegExp: regexp.MustCompile("(?i)(?:\\W|^)" + 39 | "(0{0,1}[1-9]|1[0-9]|2[0-9]|3[01])" + 40 | "[\\/\\\\]" + 41 | "(0{0,1}[1-9]|1[0-2])" + 42 | "(?:[\\/\\\\]" + 43 | "((?:1|2)[0-9]{3})\\s*)?" + 44 | "(?:\\W|$)"), 45 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 46 | if (c.Day != nil || c.Month != nil || c.Year != nil) && s != rules.Override { 47 | return false, nil 48 | } 49 | 50 | day, _ := strconv.Atoi(m.Captures[0]) 51 | month, _ := strconv.Atoi(m.Captures[1]) 52 | year := -1 53 | if m.Captures[2] != "" { 54 | year, _ = strconv.Atoi(m.Captures[2]) 55 | } 56 | 57 | if day == 0 { 58 | return false, nil 59 | } 60 | 61 | WithYear: 62 | if year != -1 { 63 | if getDays(year, month) >= day { 64 | c.Year = &year 65 | c.Month = &month 66 | c.Day = &day 67 | } else { 68 | return false, nil 69 | } 70 | return true, nil 71 | } 72 | 73 | if int(ref.Month()) > month { 74 | year = ref.Year() + 1 75 | goto WithYear 76 | } 77 | 78 | if int(ref.Month()) == month { 79 | if getDays(ref.Year(), month) >= day { 80 | if day > ref.Day() { 81 | year = ref.Year() 82 | } else if day < ref.Day() { 83 | year = ref.Year() + 1 84 | } else { 85 | return false, nil 86 | } 87 | goto WithYear 88 | } else { 89 | return false, nil 90 | } 91 | } 92 | 93 | return true, nil 94 | }, 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /rules/common/slash_dmy_test.go: -------------------------------------------------------------------------------- 1 | package common_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/common" 10 | ) 11 | 12 | func TestSlashDMY(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"The Deadline is 10/10/2016", 16, "10/10/2016", (284 - OFFSET) * 24 * time.Hour}, 15 | {"The Deadline is 1/2/2016", 16, "1/2/2016", (32 - OFFSET) * 24 * time.Hour}, 16 | {"The Deadline is 29/2/2016", 16, "29/2/2016", (60 - OFFSET) * 24 * time.Hour}, 17 | 18 | // next year 19 | {"The Deadline is 28/2", 16, "28/2", (59 + 366 - OFFSET) * 24 * time.Hour}, 20 | {"The Deadline is 28/02/2017", 16, "28/02/2017", (59 + 366 - OFFSET) * 24 * time.Hour}, 21 | 22 | // right after w/o a year 23 | {"The Deadline is 28/07", 16, "28/07", (210 - OFFSET) * 24 * time.Hour}, 24 | 25 | // before w/o a year 26 | {"The Deadline is 30/06", 16, "30/06", (181 + 366 - OFFSET) * 24 * time.Hour}, 27 | 28 | // prev day will be added to the future 29 | {"The Deadline is 14/07", 16, "14/07", (195 + 366 - OFFSET) * 24 * time.Hour}, 30 | } 31 | 32 | w := when.New(nil) 33 | w.Add(common.SlashDMY(rules.Skip)) 34 | 35 | ApplyFixtures(t, "common.SlashDMY", w, fixt) 36 | 37 | nilFixt := []Fixture{ 38 | {"The Deadline is 1/20/2016", 16, "no match for mm/dd/yyyy", 0}, 39 | } 40 | ApplyFixturesNil(t, "common.SlashDMY nil", w, nilFixt) 41 | } 42 | -------------------------------------------------------------------------------- /rules/context.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import "time" 4 | 5 | type Context struct { 6 | Text string 7 | 8 | // accumulator of relative values 9 | Duration time.Duration 10 | 11 | // Aboslute values 12 | Year, Month, Weekday, Day, Hour, Minute, Second *int 13 | 14 | Location *time.Location 15 | } 16 | 17 | func (c *Context) Time(t time.Time) (time.Time, error) { 18 | if t.IsZero() { 19 | t = time.Now() 20 | } 21 | 22 | if c.Duration != 0 { 23 | t = t.Add(c.Duration) 24 | } 25 | 26 | if c.Year != nil { 27 | t = time.Date(*c.Year, t.Month(), t.Day(), t.Hour(), 28 | t.Minute(), t.Second(), t.Nanosecond(), t.Location()) 29 | } 30 | 31 | if c.Month != nil { 32 | t = time.Date(t.Year(), time.Month(*c.Month), t.Day(), 33 | t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()) 34 | } 35 | 36 | if c.Weekday != nil { 37 | diff := int(time.Weekday(*c.Weekday) - t.Weekday()) 38 | t = time.Date(t.Year(), t.Month(), t.Day()+diff, t.Hour(), 39 | t.Minute(), t.Second(), t.Nanosecond(), t.Location()) 40 | } 41 | 42 | if c.Day != nil { 43 | t = time.Date(t.Year(), t.Month(), *c.Day, t.Hour(), 44 | t.Minute(), t.Second(), t.Nanosecond(), t.Location()) 45 | } 46 | 47 | if c.Hour != nil { 48 | t = time.Date(t.Year(), t.Month(), t.Day(), *c.Hour, 49 | t.Minute(), t.Second(), t.Nanosecond(), t.Location()) 50 | } 51 | 52 | if c.Minute != nil { 53 | t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 54 | *c.Minute, t.Second(), t.Nanosecond(), t.Location()) 55 | } 56 | 57 | if c.Second != nil { 58 | t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 59 | t.Minute(), *c.Second, t.Nanosecond(), t.Location()) 60 | } 61 | 62 | if c.Location != nil { 63 | t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 64 | t.Minute(), t.Second(), t.Nanosecond(), c.Location) 65 | } 66 | 67 | return t, nil 68 | } 69 | -------------------------------------------------------------------------------- /rules/en/casual_date.go: -------------------------------------------------------------------------------- 1 | package en 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/AlekSi/pointer" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func CasualDate(s rules.Strategy) rules.Rule { 13 | overwrite := s == rules.Override 14 | 15 | return &rules.F{ 16 | RegExp: regexp.MustCompile("(?i)(?:\\W|^)(now|today|tonight|last\\s*night|(?:tomorrow|tmr|yesterday)\\s*|tomorrow|tmr|yesterday)(?:\\W|$)"), 17 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 18 | lower := strings.ToLower(strings.TrimSpace(m.String())) 19 | 20 | switch { 21 | case strings.Contains(lower, "tonight"): 22 | if c.Hour == nil && c.Minute == nil || overwrite { 23 | c.Hour = pointer.ToInt(23) 24 | c.Minute = pointer.ToInt(0) 25 | } 26 | case strings.Contains(lower, "today"): 27 | // c.Hour = pointer.ToInt(18) 28 | case strings.Contains(lower, "tomorrow"), strings.Contains(lower, "tmr"): 29 | if c.Duration == 0 || overwrite { 30 | c.Duration += time.Hour * 24 31 | } 32 | case strings.Contains(lower, "yesterday"): 33 | if c.Duration == 0 || overwrite { 34 | c.Duration -= time.Hour * 24 35 | } 36 | case strings.Contains(lower, "last night"): 37 | if (c.Hour == nil && c.Duration == 0) || overwrite { 38 | c.Hour = pointer.ToInt(23) 39 | c.Duration -= time.Hour * 24 40 | } 41 | } 42 | 43 | return true, nil 44 | }, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rules/en/casual_test.go: -------------------------------------------------------------------------------- 1 | package en_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/en" 10 | ) 11 | 12 | func TestCasualDate(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"The Deadline is now, ok", 16, "now", 0}, 15 | {"The Deadline is today", 16, "today", 0}, 16 | {"The Deadline is tonight", 16, "tonight", 23 * time.Hour}, 17 | {"The Deadline is tomorrow evening", 16, "tomorrow", time.Hour * 24}, 18 | {"The Deadline is yesterday evening", 16, "yesterday", -(time.Hour * 24)}, 19 | } 20 | 21 | w := when.New(nil) 22 | w.Add(en.CasualDate(rules.Skip)) 23 | 24 | ApplyFixtures(t, "en.CasualDate", w, fixt) 25 | } 26 | 27 | func TestCasualTime(t *testing.T) { 28 | fixt := []Fixture{ 29 | {"The Deadline was this morning ", 17, "this morning", 8 * time.Hour}, 30 | {"The Deadline was this noon ", 17, "this noon", 12 * time.Hour}, 31 | {"The Deadline was this afternoon ", 17, "this afternoon", 15 * time.Hour}, 32 | {"The Deadline was this evening ", 17, "this evening", 18 * time.Hour}, 33 | } 34 | 35 | w := when.New(nil) 36 | w.Add(en.CasualTime(rules.Skip)) 37 | 38 | ApplyFixtures(t, "en.CasualTime", w, fixt) 39 | } 40 | 41 | func TestCasualDateCasualTime(t *testing.T) { 42 | fixt := []Fixture{ 43 | {"The Deadline is tomorrow this afternoon ", 16, "tomorrow this afternoon", (15 + 24) * time.Hour}, 44 | } 45 | 46 | w := when.New(nil) 47 | w.Add( 48 | en.CasualDate(rules.Skip), 49 | en.CasualTime(rules.Override), 50 | ) 51 | 52 | ApplyFixtures(t, "en.CasualDate|en.CasualTime", w, fixt) 53 | } 54 | -------------------------------------------------------------------------------- /rules/en/casual_time.go: -------------------------------------------------------------------------------- 1 | package en 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/AlekSi/pointer" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func CasualTime(s rules.Strategy) rules.Rule { 13 | overwrite := s == rules.Override 14 | 15 | return &rules.F{ 16 | RegExp: regexp.MustCompile(`(?i)(?:\W|^)((this)?\s*(morning|afternoon|evening|noon))`), 17 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 18 | 19 | lower := strings.ToLower(strings.TrimSpace(m.String())) 20 | 21 | if (c.Hour != nil || c.Minute != nil) && !overwrite { 22 | return false, nil 23 | } 24 | 25 | switch { 26 | case strings.Contains(lower, "afternoon"): 27 | if o.Afternoon != 0 { 28 | c.Hour = &o.Afternoon 29 | } else { 30 | c.Hour = pointer.ToInt(15) 31 | } 32 | c.Minute = pointer.ToInt(0) 33 | case strings.Contains(lower, "evening"): 34 | if o.Evening != 0 { 35 | c.Hour = &o.Evening 36 | } else { 37 | c.Hour = pointer.ToInt(18) 38 | } 39 | c.Minute = pointer.ToInt(0) 40 | case strings.Contains(lower, "morning"): 41 | if o.Morning != 0 { 42 | c.Hour = &o.Morning 43 | } else { 44 | c.Hour = pointer.ToInt(8) 45 | } 46 | c.Minute = pointer.ToInt(0) 47 | case strings.Contains(lower, "noon"): 48 | if o.Noon != 0 { 49 | c.Hour = &o.Noon 50 | } else { 51 | c.Hour = pointer.ToInt(12) 52 | } 53 | c.Minute = pointer.ToInt(0) 54 | } 55 | 56 | return true, nil 57 | }, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rules/en/deadline.go: -------------------------------------------------------------------------------- 1 | package en 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/AlekSi/pointer" 10 | "github.com/olebedev/when/rules" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func Deadline(s rules.Strategy) rules.Rule { 15 | overwrite := s == rules.Override 16 | 17 | return &rules.F{ 18 | RegExp: regexp.MustCompile( 19 | "(?i)(?:\\W|^)(within|in)\\s*" + 20 | "(" + INTEGER_WORDS_PATTERN + "|[0-9]+|an?(?:\\s*few)?|half(?:\\s*an?)?)\\s*" + 21 | "(seconds?|min(?:ute)?s?|hours?|days?|weeks?|months?|years?)\\s*" + 22 | "(?:\\W|$)"), 23 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 24 | 25 | numStr := strings.TrimSpace(m.Captures[1]) 26 | 27 | var num int 28 | var err error 29 | 30 | if n, ok := INTEGER_WORDS[numStr]; ok { 31 | num = n 32 | } else if numStr == "a" || numStr == "an" { 33 | num = 1 34 | } else if strings.Contains(numStr, "few") { 35 | num = 3 36 | } else if strings.Contains(numStr, "half") { 37 | // pass 38 | } else { 39 | num, err = strconv.Atoi(numStr) 40 | if err != nil { 41 | return false, errors.Wrapf(err, "convert '%s' to int", numStr) 42 | } 43 | } 44 | 45 | exponent := strings.TrimSpace(m.Captures[2]) 46 | 47 | if !strings.Contains(numStr, "half") { 48 | switch { 49 | case strings.Contains(exponent, "second"): 50 | if c.Duration == 0 || overwrite { 51 | c.Duration = time.Duration(num) * time.Second 52 | } 53 | case strings.Contains(exponent, "min"): 54 | if c.Duration == 0 || overwrite { 55 | c.Duration = time.Duration(num) * time.Minute 56 | } 57 | case strings.Contains(exponent, "hour"): 58 | if c.Duration == 0 || overwrite { 59 | c.Duration = time.Duration(num) * time.Hour 60 | } 61 | case strings.Contains(exponent, "day"): 62 | if c.Duration == 0 || overwrite { 63 | c.Duration = time.Duration(num) * 24 * time.Hour 64 | } 65 | case strings.Contains(exponent, "week"): 66 | if c.Duration == 0 || overwrite { 67 | c.Duration = time.Duration(num) * 7 * 24 * time.Hour 68 | } 69 | case strings.Contains(exponent, "month"): 70 | if c.Month == nil || overwrite { 71 | c.Month = pointer.ToInt((int(ref.Month()) + num) % 12) 72 | } 73 | case strings.Contains(exponent, "year"): 74 | if c.Year == nil || overwrite { 75 | c.Year = pointer.ToInt(ref.Year() + num) 76 | } 77 | } 78 | } else { 79 | switch { 80 | case strings.Contains(exponent, "hour"): 81 | if c.Duration == 0 || overwrite { 82 | c.Duration = 30 * time.Minute 83 | } 84 | case strings.Contains(exponent, "day"): 85 | if c.Duration == 0 || overwrite { 86 | c.Duration = 12 * time.Hour 87 | } 88 | case strings.Contains(exponent, "week"): 89 | if c.Duration == 0 || overwrite { 90 | c.Duration = 7 * 12 * time.Hour 91 | } 92 | case strings.Contains(exponent, "month"): 93 | if c.Duration == 0 || overwrite { 94 | // 2 weeks 95 | c.Duration = 14 * 24 * time.Hour 96 | } 97 | case strings.Contains(exponent, "year"): 98 | if c.Month == nil || overwrite { 99 | c.Month = pointer.ToInt((int(ref.Month()) + 6) % 12) 100 | } 101 | } 102 | } 103 | 104 | return true, nil 105 | }, 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /rules/en/deadline_test.go: -------------------------------------------------------------------------------- 1 | package en_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/en" 10 | ) 11 | 12 | func TestDeadline(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"within half an hour", 0, "within half an hour", time.Hour / 2}, 15 | {"within 1 hour", 0, "within 1 hour", time.Hour}, 16 | {"in 5 minutes", 0, "in 5 minutes", time.Minute * 5}, 17 | {"In 5 minutes I will go home", 0, "In 5 minutes", time.Minute * 5}, 18 | {"we have to do something within 10 days.", 24, "within 10 days", 10 * 24 * time.Hour}, 19 | {"we have to do something in five days.", 24, "in five days", 5 * 24 * time.Hour}, 20 | {"we have to do something in 5 days.", 24, "in 5 days", 5 * 24 * time.Hour}, 21 | {"In 5 seconds A car need to move", 0, "In 5 seconds", 5 * time.Second}, 22 | {"within two weeks", 0, "within two weeks", 14 * 24 * time.Hour}, 23 | {"within a month", 0, "within a month", 31 * 24 * time.Hour}, 24 | {"within a few months", 0, "within a few months", 91 * 24 * time.Hour}, 25 | {"within one year", 0, "within one year", 366 * 24 * time.Hour}, 26 | {"in a week", 0, "in a week", 7 * 24 * time.Hour}, 27 | } 28 | 29 | w := when.New(nil) 30 | w.Add(en.Deadline(rules.Skip)) 31 | 32 | ApplyFixtures(t, "en.Deadline", w, fixt) 33 | } 34 | -------------------------------------------------------------------------------- /rules/en/en.go: -------------------------------------------------------------------------------- 1 | package en 2 | 3 | import "github.com/olebedev/when/rules" 4 | 5 | var All = []rules.Rule{ 6 | Weekday(rules.Override), 7 | CasualDate(rules.Override), 8 | CasualTime(rules.Override), 9 | Hour(rules.Override), 10 | HourMinute(rules.Override), 11 | Deadline(rules.Override), 12 | PastTime(rules.Override), 13 | ExactMonthDate(rules.Override), 14 | } 15 | 16 | var WEEKDAY_OFFSET = map[string]int{ 17 | "sunday": 0, 18 | "sun": 0, 19 | "monday": 1, 20 | "mon": 1, 21 | "tuesday": 2, 22 | "tue": 2, 23 | "wednesday": 3, 24 | "wed": 3, 25 | "thursday": 4, 26 | "thur": 4, 27 | "thu": 4, 28 | "friday": 5, 29 | "fri": 5, 30 | "saturday": 6, 31 | "sat": 6, 32 | } 33 | 34 | var WEEKDAY_OFFSET_PATTERN = "(?:sunday|sun|monday|mon|tuesday|tue|wednesday|wed|thursday|thur|thu|friday|fri|saturday|sat)" 35 | 36 | var MONTH_OFFSET = map[string]int{ 37 | "january": 1, 38 | "jan": 1, 39 | "jan.": 1, 40 | "february": 2, 41 | "feb": 2, 42 | "feb.": 2, 43 | "march": 3, 44 | "mar": 3, 45 | "mar.": 3, 46 | "april": 4, 47 | "apr": 4, 48 | "apr.": 4, 49 | "may": 5, 50 | "june": 6, 51 | "jun": 6, 52 | "jun.": 6, 53 | "july": 7, 54 | "jul": 7, 55 | "jul.": 7, 56 | "august": 8, 57 | "aug": 8, 58 | "aug.": 8, 59 | "september": 9, 60 | "sep": 9, 61 | "sep.": 9, 62 | "sept": 9, 63 | "sept.": 9, 64 | "october": 10, 65 | "oct": 10, 66 | "oct.": 10, 67 | "november": 11, 68 | "nov": 11, 69 | "nov.": 11, 70 | "december": 12, 71 | "dec": 12, 72 | "dec.": 12, 73 | } 74 | 75 | var MONTH_OFFSET_PATTERN = `(?:january|jan\.?|february|feb\.?|march|mar\.?|april|apr\.?|may|june|jun\.?|july|jul\.?|august|aug\.?|september|sept?\.?|october|oct\.?|november|nov\.?|december|dec\.?)` 76 | 77 | var INTEGER_WORDS = map[string]int{ 78 | "one": 1, 79 | "two": 2, 80 | "three": 3, 81 | "four": 4, 82 | "five": 5, 83 | "six": 6, 84 | "seven": 7, 85 | "eight": 8, 86 | "nine": 9, 87 | "ten": 10, 88 | "eleven": 11, 89 | "twelve": 12, 90 | } 91 | 92 | var INTEGER_WORDS_PATTERN = `(?:one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)` 93 | 94 | var ORDINAL_WORDS = map[string]int{ 95 | "first": 1, 96 | "1st": 1, 97 | "second": 2, 98 | "2nd": 2, 99 | "third": 3, 100 | "3rd": 3, 101 | "fourth": 4, 102 | "4th": 4, 103 | "fifth": 5, 104 | "5th": 5, 105 | "sixth": 6, 106 | "6th": 6, 107 | "seventh": 7, 108 | "7th": 7, 109 | "eighth": 8, 110 | "8th": 8, 111 | "ninth": 9, 112 | "9th": 9, 113 | "tenth": 10, 114 | "10th": 10, 115 | "eleventh": 11, 116 | "11th": 11, 117 | "twelfth": 12, 118 | "12th": 12, 119 | "thirteenth": 13, 120 | "13th": 13, 121 | "fourteenth": 14, 122 | "14th": 14, 123 | "fifteenth": 15, 124 | "15th": 15, 125 | "sixteenth": 16, 126 | "16th": 16, 127 | "seventeenth": 17, 128 | "17th": 17, 129 | "eighteenth": 18, 130 | "18th": 18, 131 | "nineteenth": 19, 132 | "19th": 19, 133 | "twentieth": 20, 134 | "20th": 20, 135 | "twenty first": 21, 136 | "twenty-first": 21, 137 | "21st": 21, 138 | "twenty second": 22, 139 | "twenty-second": 22, 140 | "22nd": 22, 141 | "twenty third": 23, 142 | "twenty-third": 23, 143 | "23rd": 23, 144 | "twenty fourth": 24, 145 | "twenty-fourth": 24, 146 | "24th": 24, 147 | "twenty fifth": 25, 148 | "twenty-fifth": 25, 149 | "25th": 25, 150 | "twenty sixth": 26, 151 | "twenty-sixth": 26, 152 | "26th": 26, 153 | "twenty seventh": 27, 154 | "twenty-seventh": 27, 155 | "27th": 27, 156 | "twenty eighth": 28, 157 | "twenty-eighth": 28, 158 | "28th": 28, 159 | "twenty ninth": 29, 160 | "twenty-ninth": 29, 161 | "29th": 29, 162 | "thirtieth": 30, 163 | "30th": 30, 164 | "thirty first": 31, 165 | "thirty-first": 31, 166 | "31st": 31, 167 | } 168 | 169 | var ORDINAL_WORDS_PATTERN = `(?:1st|first|2nd|second|3rd|third|4th|fourth|5th|fifth|6th|sixth|7th|seventh|8th|eighth|9th|ninth|10th|tenth|11th|eleventh|12th|twelfth|13th|thirteenth|14th|fourteenth|15th|fifteenth|16th|sixteenth|17th|seventeenth|18th|eighteenth|19th|nineteenth|20th|twentieth|21st|twenty[ -]first|22nd|twenty[ -]second|23rd|twenty[ -]third|24th|twenty[ -]fourth|25th|twenty[ -]fifth|26th|twenty[ -]sixth|27th|twenty[ -]seventh|28th|twenty[ -]eighth|29th|twenty[ -]ninth|30th|thirtieth|31st|thirty[ -]first)` 170 | -------------------------------------------------------------------------------- /rules/en/en_test.go: -------------------------------------------------------------------------------- 1 | package en_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules/en" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var null = time.Date(2016, time.January, 6, 0, 0, 0, 0, time.UTC) 13 | 14 | type Fixture struct { 15 | Text string 16 | Index int 17 | Phrase string 18 | Diff time.Duration 19 | } 20 | 21 | func ApplyFixtures(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 22 | for i, f := range fixt { 23 | res, err := w.Parse(f.Text, null) 24 | require.Nil(t, err, "[%s] err #%d", name, i) 25 | require.NotNil(t, res, "[%s] res #%d", name, i) 26 | require.Equal(t, f.Index, res.Index, "[%s] index #%d", name, i) 27 | require.Equal(t, f.Phrase, res.Text, "[%s] text #%d", name, i) 28 | require.Equal(t, f.Diff, res.Time.Sub(null), "[%s] diff #%d", name, i) 29 | } 30 | } 31 | 32 | func ApplyFixturesNil(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 33 | for i, f := range fixt { 34 | res, err := w.Parse(f.Text, null) 35 | require.Nil(t, err, "[%s] err #%d", name, i) 36 | require.Nil(t, res, "[%s] res #%d", name, i) 37 | } 38 | } 39 | 40 | func ApplyFixturesErr(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 41 | for i, f := range fixt { 42 | _, err := w.Parse(f.Text, null) 43 | require.NotNil(t, err, "[%s] err #%d", name, i) 44 | require.Equal(t, f.Phrase, err.Error(), "[%s] err text #%d", name, i) 45 | } 46 | } 47 | 48 | func TestAll(t *testing.T) { 49 | w := when.New(nil) 50 | w.Add(en.All...) 51 | 52 | // complex cases 53 | fixt := []Fixture{ 54 | {"tonight at 11:10 pm", 0, "tonight at 11:10 pm", (23 * time.Hour) + (10 * time.Minute)}, 55 | {"at Friday afternoon", 3, "Friday afternoon", ((2 * 24) + 15) * time.Hour}, 56 | {"in next tuesday at 14:00", 3, "next tuesday at 14:00", ((6 * 24) + 14) * time.Hour}, 57 | {"in next tuesday at 2p", 3, "next tuesday at 2p", ((6 * 24) + 14) * time.Hour}, 58 | {"in next wednesday at 2:25 p.m.", 3, "next wednesday at 2:25 p.m.", (((7 * 24) + 14) * time.Hour) + (25 * time.Minute)}, 59 | {"at 11 am past tuesday", 3, "11 am past tuesday", -13 * time.Hour}, 60 | } 61 | 62 | ApplyFixtures(t, "en.All...", w, fixt) 63 | } 64 | -------------------------------------------------------------------------------- /rules/en/exact_month_date.go: -------------------------------------------------------------------------------- 1 | package en 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | // <[]string{"third of march", "third", "", "march", "", ""}> 13 | // <[]string{"march third", "", "", "march", "third", ""}> 14 | // <[]string{"march 3rd", "", "", "march", "3rd", ""}> 15 | // <[]string{"3rd march", "3rd", "", "march", "", ""}> 16 | // <[]string{"march 3", "", "", "march", "", "3"}> 17 | // <[]string{"1st of september", "1st", "", "september", "", ""}> 18 | // <[]string{"sept. 1st", "", "", "sept.", "1st", ""}> 19 | // <[]string{"march 7th", "", "", "march", "7th", ""}> 20 | // <[]string{"october 21st", "", "", "october", "21st", ""}> 21 | // <[]string{"twentieth of december", "twentieth", "", "december", "", ""}> 22 | // <[]string{"march 10th", "", "", "march", "10th", ""}> 23 | // <[]string{"jan. 6", "", "", "jan.", "", "6"}> 24 | // <[]string{"february", "", "", "february", "", ""}> 25 | // <[]string{"october", "", "", "october", "", ""}> 26 | // <[]string{"jul.", "", "", "jul.", "", ""}> 27 | // <[]string{"june", "", "", "june", "", ""}> 28 | 29 | // https://play.golang.org/p/Zfjl6ERNkq 30 | 31 | // 1. - ordinal day? 32 | // 2. - numeric day? 33 | // 3. - month 34 | // 4. - ordinal day? 35 | // 5. - ordinal day? 36 | 37 | func ExactMonthDate(s rules.Strategy) rules.Rule { 38 | overwrite := s == rules.Override 39 | 40 | return &rules.F{ 41 | RegExp: regexp.MustCompile("(?i)" + 42 | "(?:\\W|^)" + 43 | "(?:(?:(" + ORDINAL_WORDS_PATTERN[3:] + "(?:\\s+of)?|([0-9]+))\\s*)?" + 44 | "(" + MONTH_OFFSET_PATTERN[3:] + // skip '(?:' 45 | "(?:\\s*(?:(" + ORDINAL_WORDS_PATTERN[3:] + "|([0-9]+)))?" + 46 | "(?:\\W|$)", 47 | ), 48 | 49 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 50 | _ = overwrite 51 | 52 | ord1 := strings.ToLower(strings.TrimSpace(m.Captures[0])) 53 | num1 := strings.ToLower(strings.TrimSpace(m.Captures[1])) 54 | mon := strings.ToLower(strings.TrimSpace(m.Captures[2])) 55 | ord2 := strings.ToLower(strings.TrimSpace(m.Captures[3])) 56 | num2 := strings.ToLower(strings.TrimSpace(m.Captures[4])) 57 | 58 | monInt, ok := MONTH_OFFSET[mon] 59 | if !ok { 60 | return false, nil 61 | } 62 | 63 | c.Month = &monInt 64 | 65 | if ord1 != "" { 66 | ordInt, ok := ORDINAL_WORDS[ord1] 67 | if !ok { 68 | return false, nil 69 | } 70 | 71 | c.Day = &ordInt 72 | } 73 | 74 | if num1 != "" { 75 | n, err := strconv.ParseInt(num1, 10, 8) 76 | if err != nil { 77 | return false, nil 78 | } 79 | 80 | num := int(n) 81 | 82 | c.Day = &num 83 | } 84 | 85 | if ord2 != "" { 86 | ordInt, ok := ORDINAL_WORDS[ord2] 87 | if !ok { 88 | return false, nil 89 | } 90 | 91 | c.Day = &ordInt 92 | } 93 | 94 | if num2 != "" { 95 | n, err := strconv.ParseInt(num2, 10, 8) 96 | if err != nil { 97 | return false, nil 98 | } 99 | 100 | num := int(n) 101 | 102 | c.Day = &num 103 | } 104 | 105 | return true, nil 106 | }, 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /rules/en/exact_month_date_test.go: -------------------------------------------------------------------------------- 1 | package en_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/en" 10 | ) 11 | 12 | func TestExactMonthDate(t *testing.T) { 13 | w := when.New(nil) 14 | w.Add(en.ExactMonthDate(rules.Override)) 15 | 16 | fixtok := []Fixture{ 17 | {"third of march", 0, "third of march", 1368 * time.Hour}, 18 | {"march third", 0, "march third", 1368 * time.Hour}, 19 | {"march 3rd", 0, "march 3rd", 1368 * time.Hour}, 20 | {"3rd march", 0, "3rd march", 1368 * time.Hour}, 21 | {"march 3", 0, "march 3", 1368 * time.Hour}, 22 | {"1 september", 0, "1 september", 5736 * time.Hour}, 23 | {"1 sept", 0, "1 sept", 5736 * time.Hour}, 24 | {"1 sept.", 0, "1 sept.", 5736 * time.Hour}, 25 | {"1st of september", 0, "1st of september", 5736 * time.Hour}, 26 | {"sept. 1st", 0, "sept. 1st", 5736 * time.Hour}, 27 | {"march 7th", 0, "march 7th", 1464 * time.Hour}, 28 | {"october 21st", 0, "october 21st", 6936 * time.Hour}, 29 | {"twentieth of december", 0, "twentieth of december", 8376 * time.Hour}, 30 | {"march 10th", 0, "march 10th", 1536 * time.Hour}, 31 | {"jan. 4", 0, "jan. 4", -48 * time.Hour}, 32 | {"february", 0, "february", 744 * time.Hour}, 33 | {"october", 0, "october", 6576 * time.Hour}, 34 | {"jul.", 0, "jul.", 4368 * time.Hour}, 35 | {"june", 0, "june", 3648 * time.Hour}, 36 | } 37 | 38 | ApplyFixtures(t, "en.ExactMonthDate", w, fixtok) 39 | } 40 | -------------------------------------------------------------------------------- /rules/en/hour.go: -------------------------------------------------------------------------------- 1 | package en 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | 10 | "github.com/olebedev/when/rules" 11 | ) 12 | 13 | /* 14 | "5pm" 15 | "5 pm" 16 | "5am" 17 | "5pm" 18 | "5A." 19 | "5P." 20 | "11 P.M." 21 | https://play.golang.org/p/2Gh35Sl3KP 22 | */ 23 | 24 | func Hour(s rules.Strategy) rules.Rule { 25 | 26 | return &rules.F{ 27 | RegExp: regexp.MustCompile("(?i)(?:\\W|^)" + 28 | "(\\d{1,2})" + 29 | "(?:\\s*(A\\.|P\\.|A\\.M\\.|P\\.M\\.|AM?|PM?))" + 30 | "(?:\\W|$)"), 31 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 32 | if c.Hour != nil && s != rules.Override { 33 | return false, nil 34 | } 35 | 36 | hour, err := strconv.Atoi(m.Captures[0]) 37 | if err != nil { 38 | return false, errors.Wrap(err, "hour rule") 39 | } 40 | 41 | if hour > 12 { 42 | return false, nil 43 | } 44 | 45 | zero := 0 46 | switch m.Captures[1][0] { 47 | case 65, 97: // am 48 | c.Hour = &hour 49 | case 80, 112: // pm 50 | if hour < 12 { 51 | hour += 12 52 | } 53 | c.Hour = &hour 54 | } 55 | 56 | c.Minute = &zero 57 | c.Second = &zero 58 | return true, nil 59 | }, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rules/en/hour_minute.go: -------------------------------------------------------------------------------- 1 | package en 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | /* 13 | {"5:30pm", 0, "5:30pm", 0}, 14 | {"5:30 pm", 0, "5:30 pm", 0}, 15 | {"7-10pm", 0, "7-10pm", 0}, 16 | {"5-30", 0, "5-30", 0}, 17 | {"05:30pm", 0, "05:30pm", 0}, 18 | {"05:30 pm", 0, "05:30 pm", 0}, 19 | {"05:30", 0, "05:30", 0}, 20 | {"05-30", 0, "05-30", 0}, 21 | {"7-10 pm", 0, "7-10 pm", 0}, 22 | {"11.10 pm", 0, "11.10 pm", 0}, 23 | 24 | https://play.golang.org/p/hXl7C8MWNr 25 | */ 26 | 27 | // 1. - int 28 | // 2. - int 29 | // 3. - ext? 30 | 31 | func HourMinute(s rules.Strategy) rules.Rule { 32 | return &rules.F{ 33 | RegExp: regexp.MustCompile("(?i)(?:\\W|^)" + 34 | "((?:[0-1]{0,1}[0-9])|(?:2[0-3]))" + 35 | "(?:\\:|:|\\-)" + 36 | "((?:[0-5][0-9]))" + 37 | "(?:\\s*(A\\.|P\\.|A\\.M\\.|P\\.M\\.|AM?|PM?))?" + 38 | "(?:\\W|$)"), 39 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 40 | if (c.Hour != nil || c.Minute != nil) && s != rules.Override { 41 | return false, nil 42 | } 43 | 44 | hour, err := strconv.Atoi(m.Captures[0]) 45 | if err != nil { 46 | return false, errors.Wrap(err, "hour minute rule") 47 | } 48 | 49 | minutes, err := strconv.Atoi(m.Captures[1]) 50 | if err != nil { 51 | return false, errors.Wrap(err, "hour minute rule") 52 | } 53 | 54 | if minutes > 59 { 55 | return false, nil 56 | } 57 | c.Minute = &minutes 58 | 59 | if m.Captures[2] != "" { 60 | if hour > 12 { 61 | return false, nil 62 | } 63 | switch m.Captures[2][0] { 64 | case 65, 97: // am 65 | c.Hour = &hour 66 | case 80, 112: // pm 67 | if hour < 12 { 68 | hour += 12 69 | } 70 | c.Hour = &hour 71 | } 72 | } else { 73 | if hour > 23 { 74 | return false, nil 75 | } 76 | c.Hour = &hour 77 | } 78 | seconds := 0 // Truncate seconds 79 | c.Second = &seconds 80 | 81 | return true, nil 82 | }, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /rules/en/hour_minute_test.go: -------------------------------------------------------------------------------- 1 | package en_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/en" 10 | ) 11 | 12 | func TestHourMinute(t *testing.T) { 13 | w := when.New(nil) 14 | w.Add(en.HourMinute(rules.Override)) 15 | 16 | fixtok := []Fixture{ 17 | {"5:30pm", 0, "5:30pm", (17 * time.Hour) + (30 * time.Minute)}, 18 | {"at 5:30 pm", 3, "5:30 pm", (17 * time.Hour) + (30 * time.Minute)}, 19 | {"at 5:59 pm", 3, "5:59 pm", (17 * time.Hour) + (59 * time.Minute)}, 20 | {"at 5-59 pm", 3, "5-59 pm", (17 * time.Hour) + (59 * time.Minute)}, 21 | {"at 17-59 pam", 3, "17-59", (17 * time.Hour) + (59 * time.Minute)}, 22 | {"up to 11:10 pm", 6, "11:10 pm", (23 * time.Hour) + (10 * time.Minute)}, 23 | } 24 | 25 | fixtnil := []Fixture{ 26 | {"28:30pm", 0, "", 0}, 27 | {"12:61pm", 0, "", 0}, 28 | {"24:10", 0, "", 0}, 29 | } 30 | 31 | ApplyFixtures(t, "en.HourMinute", w, fixtok) 32 | ApplyFixturesNil(t, "on.HourMinute nil", w, fixtnil) 33 | 34 | w.Add(en.Hour(rules.Skip)) 35 | ApplyFixtures(t, "en.HourMinute|en.Hour", w, fixtok) 36 | ApplyFixturesNil(t, "on.HourMinute|en.Hour nil", w, fixtnil) 37 | 38 | w = when.New(nil) 39 | w.Add( 40 | en.Hour(rules.Override), 41 | en.HourMinute(rules.Override), 42 | ) 43 | 44 | ApplyFixtures(t, "en.Hour|en.HourMinute", w, fixtok) 45 | ApplyFixturesNil(t, "on.Hour|en.HourMinute nil", w, fixtnil) 46 | } 47 | -------------------------------------------------------------------------------- /rules/en/hour_test.go: -------------------------------------------------------------------------------- 1 | package en_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/en" 10 | ) 11 | 12 | func TestHour(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"5pm", 0, "5pm", 17 * time.Hour}, 15 | {"at 5 pm", 3, "5 pm", 17 * time.Hour}, 16 | {"at 5 P.", 3, "5 P.", 17 * time.Hour}, 17 | {"at 12 P.", 3, "12 P.", 12 * time.Hour}, 18 | {"at 1 P.", 3, "1 P.", 13 * time.Hour}, 19 | {"at 5 am", 3, "5 am", 5 * time.Hour}, 20 | {"at 5A", 3, "5A", 5 * time.Hour}, 21 | {"at 5A.", 3, "5A.", 5 * time.Hour}, 22 | {"5A.", 0, "5A.", 5 * time.Hour}, 23 | {"11 P.M.", 0, "11 P.M.", 23 * time.Hour}, 24 | } 25 | 26 | w := when.New(nil) 27 | w.Add(en.Hour(rules.Override)) 28 | 29 | ApplyFixtures(t, "en.Hour", w, fixt) 30 | } 31 | -------------------------------------------------------------------------------- /rules/en/past_time.go: -------------------------------------------------------------------------------- 1 | package en 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/AlekSi/pointer" 10 | "github.com/olebedev/when/rules" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func PastTime(s rules.Strategy) rules.Rule { 15 | overwrite := s == rules.Override 16 | 17 | return &rules.F{ 18 | RegExp: regexp.MustCompile( 19 | "(?i)(?:\\W|^)\\s*" + 20 | "(" + INTEGER_WORDS_PATTERN + "|[0-9]+|an?(?:\\s*few)?|half(?:\\s*an?)?)\\s*" + 21 | "(seconds?|min(?:ute)?s?|hours?|days?|weeks?|months?|years?) (ago)\\s*" + 22 | "(?:\\W|$)"), 23 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 24 | 25 | numStr := strings.TrimSpace(m.Captures[0]) 26 | 27 | var num int 28 | var err error 29 | 30 | if n, ok := INTEGER_WORDS[numStr]; ok { 31 | num = n 32 | } else if numStr == "a" || numStr == "an" { 33 | num = 1 34 | } else if strings.Contains(numStr, "few") { 35 | num = 3 36 | } else if strings.Contains(numStr, "half") { 37 | // pass 38 | } else { 39 | num, err = strconv.Atoi(numStr) 40 | if err != nil { 41 | return false, errors.Wrapf(err, "convert '%s' to int", numStr) 42 | } 43 | } 44 | 45 | exponent := strings.TrimSpace(m.Captures[1]) 46 | 47 | if !strings.Contains(numStr, "half") { 48 | switch { 49 | case strings.Contains(exponent, "second"): 50 | if c.Duration == 0 || overwrite { 51 | c.Duration = -(time.Duration(num) * time.Second) 52 | } 53 | case strings.Contains(exponent, "min"): 54 | if c.Duration == 0 || overwrite { 55 | c.Duration = -(time.Duration(num) * time.Minute) 56 | } 57 | case strings.Contains(exponent, "hour"): 58 | if c.Duration == 0 || overwrite { 59 | c.Duration = -(time.Duration(num) * time.Hour) 60 | } 61 | case strings.Contains(exponent, "day"): 62 | if c.Duration == 0 || overwrite { 63 | c.Duration = -(time.Duration(num) * 24 * time.Hour) 64 | } 65 | case strings.Contains(exponent, "week"): 66 | if c.Duration == 0 || overwrite { 67 | c.Duration = -(time.Duration(num) * 7 * 24 * time.Hour) 68 | } 69 | case strings.Contains(exponent, "month"): 70 | if c.Month == nil || overwrite { 71 | c.Month = pointer.ToInt((int(ref.Month()) - num) % 12) 72 | } 73 | case strings.Contains(exponent, "year"): 74 | if c.Year == nil || overwrite { 75 | c.Year = pointer.ToInt(ref.Year() - num) 76 | } 77 | } 78 | } else { 79 | switch { 80 | case strings.Contains(exponent, "hour"): 81 | if c.Duration == 0 || overwrite { 82 | c.Duration = -(30 * time.Minute) 83 | } 84 | case strings.Contains(exponent, "day"): 85 | if c.Duration == 0 || overwrite { 86 | c.Duration = -(12 * time.Hour) 87 | } 88 | case strings.Contains(exponent, "week"): 89 | if c.Duration == 0 || overwrite { 90 | c.Duration = -(7 * 12 * time.Hour) 91 | } 92 | case strings.Contains(exponent, "month"): 93 | if c.Duration == 0 || overwrite { 94 | // 2 weeks 95 | c.Duration = -(14 * 24 * time.Hour) 96 | } 97 | case strings.Contains(exponent, "year"): 98 | if c.Month == nil || overwrite { 99 | c.Month = pointer.ToInt((int(ref.Month()) - 6) % 12) 100 | } 101 | } 102 | } 103 | 104 | return true, nil 105 | }, 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /rules/en/past_time_test.go: -------------------------------------------------------------------------------- 1 | package en_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/en" 10 | ) 11 | 12 | func TestPastTime(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"half an hour ago", 0, "half an hour ago", -(time.Hour / 2)}, 15 | {"1 hour ago", 0, "1 hour ago", -(time.Hour)}, 16 | {"5 minutes ago", 0, "5 minutes ago", -(time.Minute * 5)}, 17 | {"5 minutes ago I went to the zoo", 0, "5 minutes ago", -(time.Minute * 5)}, 18 | {"we did something 10 days ago.", 17, "10 days ago", -(10 * 24 * time.Hour)}, 19 | {"we did something five days ago.", 17, "five days ago", -(5 * 24 * time.Hour)}, 20 | {"we did something 5 days ago.", 17, "5 days ago", -(5 * 24 * time.Hour)}, 21 | {"5 seconds ago a car was moved", 0, "5 seconds ago", -(5 * time.Second)}, 22 | {"two weeks ago", 0, "two weeks ago", -(14 * 24 * time.Hour)}, 23 | {"a month ago", 0, "a month ago", -(31 * 24 * time.Hour)}, 24 | {"a few months ago", 0, "a few months ago", -(92 * 24 * time.Hour)}, 25 | {"one year ago", 0, "one year ago", -(365 * 24 * time.Hour)}, 26 | {"a week ago", 0, "a week ago", -(7 * 24 * time.Hour)}, 27 | } 28 | 29 | w := when.New(nil) 30 | w.Add(en.PastTime(rules.Skip)) 31 | 32 | ApplyFixtures(t, "en.PastTime", w, fixt) 33 | } 34 | -------------------------------------------------------------------------------- /rules/en/weekday.go: -------------------------------------------------------------------------------- 1 | package en 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | ) 10 | 11 | func Weekday(s rules.Strategy) rules.Rule { 12 | overwrite := s == rules.Override 13 | 14 | return &rules.F{ 15 | RegExp: regexp.MustCompile("(?i)" + 16 | "(?:\\W|^)" + 17 | "(?:on\\s*?)?" + 18 | "(?:(this|last|past|next)\\s*)?" + 19 | "(" + WEEKDAY_OFFSET_PATTERN[3:] + // skip '(?:' 20 | "(?:\\s*(this|last|past|next)\\s*week)?" + 21 | "(?:\\W|$)", 22 | ), 23 | 24 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 25 | _ = overwrite 26 | 27 | day := strings.ToLower(strings.TrimSpace(m.Captures[1])) 28 | norm := strings.ToLower(strings.TrimSpace(m.Captures[0] + m.Captures[2])) 29 | if norm == "" { 30 | norm = "next" 31 | } 32 | dayInt, ok := WEEKDAY_OFFSET[day] 33 | if !ok { 34 | return false, nil 35 | } 36 | 37 | if c.Duration != 0 && !overwrite { 38 | return false, nil 39 | } 40 | 41 | // Switch: 42 | switch { 43 | case strings.Contains(norm, "past") || strings.Contains(norm, "last"): 44 | diff := int(ref.Weekday()) - dayInt 45 | if diff > 0 { 46 | c.Duration = -time.Duration(diff*24) * time.Hour 47 | } else if diff < 0 { 48 | c.Duration = -time.Duration(7+diff) * 24 * time.Hour 49 | } else { 50 | c.Duration = -(7 * 24 * time.Hour) 51 | } 52 | case strings.Contains(norm, "next"): 53 | diff := dayInt - int(ref.Weekday()) 54 | if diff > 0 { 55 | c.Duration = time.Duration(diff*24) * time.Hour 56 | } else if diff < 0 { 57 | c.Duration = time.Duration(7+diff) * 24 * time.Hour 58 | } else { 59 | c.Duration = 7 * 24 * time.Hour 60 | } 61 | case strings.Contains(norm, "this"): 62 | if int(ref.Weekday()) < dayInt { 63 | diff := dayInt - int(ref.Weekday()) 64 | if diff > 0 { 65 | c.Duration = time.Duration(diff*24) * time.Hour 66 | } else if diff < 0 { 67 | c.Duration = time.Duration(7+diff) * 24 * time.Hour 68 | } else { 69 | c.Duration = 7 * 24 * time.Hour 70 | } 71 | } else if int(ref.Weekday()) > dayInt { 72 | diff := int(ref.Weekday()) - dayInt 73 | if diff > 0 { 74 | c.Duration = -time.Duration(diff*24) * time.Hour 75 | } else if diff < 0 { 76 | c.Duration = -time.Duration(7+diff) * 24 * time.Hour 77 | } else { 78 | c.Duration = -(7 * 24 * time.Hour) 79 | } 80 | } 81 | } 82 | 83 | return true, nil 84 | }, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /rules/en/weekday_test.go: -------------------------------------------------------------------------------- 1 | package en_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/en" 10 | ) 11 | 12 | func TestWeekday(t *testing.T) { 13 | // current is Friday 14 | fixt := []Fixture{ 15 | // past/last 16 | {"do it for the past Monday", 14, "past Monday", -(2 * 24 * time.Hour)}, 17 | {"past saturday", 0, "past saturday", -(4 * 24 * time.Hour)}, 18 | {"past friday", 0, "past friday", -(5 * 24 * time.Hour)}, 19 | {"past wednesday", 0, "past wednesday", -(7 * 24 * time.Hour)}, 20 | {"past tuesday", 0, "past tuesday", -(24 * time.Hour)}, 21 | // next 22 | {"next tuesday", 0, "next tuesday", 6 * 24 * time.Hour}, 23 | {"drop me a line at next wednesday", 18, "next wednesday", 7 * 24 * time.Hour}, 24 | {"next saturday", 0, "next saturday", 3 * 24 * time.Hour}, 25 | // this 26 | {"this tuesday", 0, "this tuesday", -(24 * time.Hour)}, 27 | {"drop me a line at this wednesday", 18, "this wednesday", 0}, 28 | {"this saturday", 0, "this saturday", 3 * 24 * time.Hour}, 29 | } 30 | 31 | w := when.New(nil) 32 | 33 | w.Add(en.Weekday(rules.Override)) 34 | 35 | ApplyFixtures(t, "en.Weekday", w, fixt) 36 | } 37 | -------------------------------------------------------------------------------- /rules/nl/casual_date.go: -------------------------------------------------------------------------------- 1 | package nl 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/AlekSi/pointer" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func CasualDate(s rules.Strategy) rules.Rule { 13 | overwrite := s == rules.Override 14 | 15 | return &rules.F{ 16 | RegExp: regexp.MustCompile("(?i)(?:\\W|^)(nu|vandaag|vanavond|vannacht|afgelopen\\s*nacht|morgen|gister|gisteren)(ochtend|morgen|middag|avond)?(?:\\W|$)"), 17 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 18 | lower := strings.ToLower(strings.TrimSpace(m.String())) 19 | 20 | if regexp.MustCompile("ochtend|\\s*morgen|middag|avond").MatchString(lower) { 21 | switch { 22 | case strings.Contains(lower, "ochtend"), regexp.MustCompile("(?i)(?:\\W|^)(\\s*morgen)(?:\\W|$)").MatchString(lower): 23 | if o.Morning != 0 { 24 | c.Hour = &o.Morning 25 | } else { 26 | c.Hour = pointer.ToInt(8) 27 | } 28 | case strings.Contains(lower, "middag"): 29 | if o.Afternoon != 0 { 30 | c.Hour = &o.Afternoon 31 | } else { 32 | c.Hour = pointer.ToInt(15) 33 | } 34 | case strings.Contains(lower, "avond"): 35 | if o.Evening != 0 { 36 | c.Hour = &o.Evening 37 | } else { 38 | c.Hour = pointer.ToInt(18) 39 | } 40 | } 41 | } 42 | 43 | switch { 44 | case strings.Contains(lower, "vannacht"): 45 | if c.Hour == nil && c.Minute == nil || overwrite { 46 | c.Hour = pointer.ToInt(23) 47 | c.Minute = pointer.ToInt(0) 48 | } 49 | case strings.Contains(lower, "vandaag"): 50 | // c.Hour = pointer.ToInt(18) 51 | case strings.Contains(lower, "morgen"): 52 | if c.Duration == 0 || overwrite { 53 | c.Duration += time.Hour * 24 54 | } 55 | case strings.Contains(lower, "gister"): 56 | if c.Duration == 0 || overwrite { 57 | c.Duration -= time.Hour * 24 58 | } 59 | case strings.Contains(lower, "afgelopen nacht"): 60 | if (c.Hour == nil && c.Duration == 0) || overwrite { 61 | c.Hour = pointer.ToInt(23) 62 | c.Duration -= time.Hour * 24 63 | } 64 | } 65 | 66 | return true, nil 67 | }, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /rules/nl/casual_test.go: -------------------------------------------------------------------------------- 1 | package nl_test 2 | 3 | import ( 4 | "github.com/olebedev/when/rules/nl" 5 | "testing" 6 | "time" 7 | 8 | "github.com/olebedev/when" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func TestCasualDate(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"De deadline is nu, ok", 15, "nu", 0}, 15 | {"De deadline is vandaag", 15, "vandaag", 0}, 16 | {"De deadline is vannacht", 15, "vannacht", 23 * time.Hour}, 17 | {"De deadline is morgenavond", 15, "morgenavond", (18 + 24) * time.Hour}, 18 | {"De deadline is gisteravond", 15, "gisteravond", -((24 - 18) * time.Hour)}, 19 | {"De deadline is gisteren", 15, "gisteren", -(time.Hour * 24)}, 20 | } 21 | 22 | w := when.New(nil) 23 | w.Add(nl.CasualDate(rules.Skip)) 24 | 25 | ApplyFixtures(t, "nl.CasualDate", w, fixt) 26 | } 27 | 28 | func TestCasualTime(t *testing.T) { 29 | fixt := []Fixture{ 30 | {"De deadline was deze morgen", 16, "deze morgen", 8 * time.Hour}, 31 | {"De deadline was tussen de middag", 16, "tussen de middag", 12 * time.Hour}, 32 | {"De deadline was deze middag", 16, "deze middag", 15 * time.Hour}, 33 | {"De deadline was deze avond", 16, "deze avond", 18 * time.Hour}, 34 | {"De deadline is donderdagavond", 15, "donderdagavond", (18 + 24) * time.Hour}, 35 | {"De deadline is vrijdagavond", 15, "vrijdagavond", (18 + 24*2) * time.Hour}, 36 | } 37 | 38 | w := when.New(nil) 39 | w.Add(nl.CasualTime(rules.Skip)) 40 | 41 | ApplyFixtures(t, "nl.CasualTime", w, fixt) 42 | } 43 | 44 | func TestCasualDateCasualTime(t *testing.T) { 45 | fixt := []Fixture{ 46 | {"De deadline is morgenmiddag", 15, "morgenmiddag", (15 + 24) * time.Hour}, 47 | {"De deadline is morgenavond", 15, "morgenavond", (18 + 24) * time.Hour}, 48 | } 49 | 50 | w := when.New(nil) 51 | w.Add( 52 | nl.CasualDate(rules.Skip), 53 | nl.CasualTime(rules.Override), 54 | ) 55 | 56 | ApplyFixtures(t, "nl.CasualDate|nl.CasualTime", w, fixt) 57 | } 58 | -------------------------------------------------------------------------------- /rules/nl/casual_time.go: -------------------------------------------------------------------------------- 1 | package nl 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/AlekSi/pointer" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func CasualTime(s rules.Strategy) rules.Rule { 13 | overwrite := s == rules.Override 14 | 15 | return &rules.F{ 16 | RegExp: regexp.MustCompile(`(?i)(?:\W|^)((deze|tussen de |maandag|dinsdag|woensdag|donderdag|vrijdag|zaterdag|zondag| )\s*(ochtend|morgen|middag|avond))`), 17 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 18 | 19 | lower := strings.ToLower(strings.TrimSpace(m.String())) 20 | 21 | if (c.Weekday != nil || c.Hour != nil || c.Minute != nil) && !overwrite { 22 | return false, nil 23 | } 24 | 25 | if regexp.MustCompile("(maandag|dinsdag|woensdag|donderdag|vrijdag|zaterdag|zondag)").MatchString(lower) { 26 | weekday := -1 27 | 28 | switch { 29 | case strings.Contains(lower, "maandag"): 30 | weekday = 1 31 | case strings.Contains(lower, "dinsdag"): 32 | weekday = 2 33 | case strings.Contains(lower, "woensdag"): 34 | weekday = 3 35 | case strings.Contains(lower, "donderdag"): 36 | weekday = 4 37 | case strings.Contains(lower, "vrijdag"): 38 | weekday = 5 39 | case strings.Contains(lower, "zaterdag"): 40 | weekday = 6 41 | case strings.Contains(lower, "zondag"): 42 | weekday = 7 43 | } 44 | 45 | if weekday != -1 { 46 | c.Duration += time.Hour * 24 * time.Duration((weekday+7-(int(ref.Weekday())))%7) 47 | } 48 | } 49 | 50 | switch { 51 | case strings.Contains(lower, "middag") && !strings.Contains(lower, "tussen de middag"): 52 | if o.Afternoon != 0 { 53 | c.Hour = &o.Afternoon 54 | } else { 55 | c.Hour = pointer.ToInt(15) 56 | } 57 | c.Minute = pointer.ToInt(0) 58 | case strings.Contains(lower, "avond"): 59 | if o.Evening != 0 { 60 | c.Hour = &o.Evening 61 | } else { 62 | c.Hour = pointer.ToInt(18) 63 | } 64 | c.Minute = pointer.ToInt(0) 65 | case strings.Contains(lower, "ochtend"), strings.Contains(lower, "morgen"): 66 | if o.Morning != 0 { 67 | c.Hour = &o.Morning 68 | } else { 69 | c.Hour = pointer.ToInt(8) 70 | } 71 | c.Minute = pointer.ToInt(0) 72 | case strings.Contains(lower, "tussen de middag"): 73 | c.Hour = pointer.ToInt(12) 74 | c.Minute = pointer.ToInt(0) 75 | } 76 | 77 | return true, nil 78 | }, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /rules/nl/deadline.go: -------------------------------------------------------------------------------- 1 | package nl 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/AlekSi/pointer" 10 | "github.com/olebedev/when/rules" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func Deadline(s rules.Strategy) rules.Rule { 15 | overwrite := s == rules.Override 16 | 17 | return &rules.F{ 18 | RegExp: regexp.MustCompile( 19 | "(?i)(?:\\W|^)(binnen|in|over|na)\\s*" + 20 | "(" + INTEGER_WORDS_PATTERN + "|[0-9]+|een(?:\\s*(paar|half|halve))?)\\s*" + 21 | "(seconden?|minuut|minuten|uur|uren|dag|dagen|week|weken|maand|maanden|jaar|jaren)\\s*" + 22 | "(?:\\W|$)"), 23 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 24 | numStr := strings.TrimSpace(m.Captures[1]) 25 | 26 | var num int 27 | var err error 28 | 29 | if n, ok := INTEGER_WORDS[numStr]; ok { 30 | num = n 31 | } else if numStr == "een" { 32 | num = 1 33 | } else if strings.Contains(numStr, "paar") { 34 | num = 3 35 | } else if strings.Contains(numStr, "half") || strings.Contains(numStr, "halve") { 36 | // pass 37 | } else { 38 | num, err = strconv.Atoi(numStr) 39 | if err != nil { 40 | return false, errors.Wrapf(err, "convert '%s' to int", numStr) 41 | } 42 | } 43 | 44 | exponent := strings.TrimSpace(m.Captures[3]) 45 | 46 | if !strings.Contains(numStr, "half") && !strings.Contains(numStr, "halve") { 47 | switch { 48 | case strings.Contains(exponent, "second"): 49 | if c.Duration == 0 || overwrite { 50 | c.Duration = time.Duration(num) * time.Second 51 | } 52 | case strings.Contains(exponent, "min"): 53 | if c.Duration == 0 || overwrite { 54 | c.Duration = time.Duration(num) * time.Minute 55 | } 56 | case strings.Contains(exponent, "uur"), strings.Contains(exponent, "uren"): 57 | if c.Duration == 0 || overwrite { 58 | c.Duration = time.Duration(num) * time.Hour 59 | } 60 | case strings.Contains(exponent, "dag"): 61 | if c.Duration == 0 || overwrite { 62 | c.Duration = time.Duration(num) * 24 * time.Hour 63 | } 64 | case strings.Contains(exponent, "week"), strings.Contains(exponent, "weken"): 65 | if c.Duration == 0 || overwrite { 66 | c.Duration = time.Duration(num) * 7 * 24 * time.Hour 67 | } 68 | case strings.Contains(exponent, "maand"): 69 | if c.Month == nil || overwrite { 70 | c.Month = pointer.ToInt((int(ref.Month()) + num) % 12) 71 | } 72 | case strings.Contains(exponent, "jaar"): 73 | if c.Year == nil || overwrite { 74 | c.Year = pointer.ToInt(ref.Year() + num) 75 | } 76 | } 77 | } else { 78 | switch { 79 | case strings.Contains(exponent, "uur"): 80 | if c.Duration == 0 || overwrite { 81 | c.Duration = 30 * time.Minute 82 | } 83 | case strings.Contains(exponent, "dag"): 84 | if c.Duration == 0 || overwrite { 85 | c.Duration = 12 * time.Hour 86 | } 87 | case strings.Contains(exponent, "week"): 88 | if c.Duration == 0 || overwrite { 89 | c.Duration = 7 * 12 * time.Hour 90 | } 91 | case strings.Contains(exponent, "maand"): 92 | if c.Duration == 0 || overwrite { 93 | // 2 weeks 94 | c.Duration = 14 * 24 * time.Hour 95 | } 96 | case strings.Contains(exponent, "jaar"): 97 | if c.Month == nil || overwrite { 98 | c.Month = pointer.ToInt((int(ref.Month()) + 6) % 12) 99 | } 100 | } 101 | } 102 | 103 | return true, nil 104 | }, 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /rules/nl/deadline_test.go: -------------------------------------------------------------------------------- 1 | package nl_test 2 | 3 | import ( 4 | "github.com/olebedev/when/rules/nl" 5 | "testing" 6 | "time" 7 | 8 | "github.com/olebedev/when" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func TestDeadline(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"binnen een half uur", 0, "binnen een half uur", time.Hour / 2}, 15 | {"binnen 1 uur", 0, "binnen 1 uur", time.Hour}, 16 | {"in 5 minuten", 0, "in 5 minuten", time.Minute * 5}, 17 | {"Binnen 5 minuten ga ik naar huis", 0, "Binnen 5 minuten", time.Minute * 5}, 18 | {"we moeten binnen 10 dagen iets doen", 10, "binnen 10 dagen", 10 * 24 * time.Hour}, 19 | {"we moeten binnen vijf dagen iets doen", 10, "binnen vijf dagen", 5 * 24 * time.Hour}, 20 | {"we moeten over 5 dagen iets doen", 10, "over 5 dagen", 5 * 24 * time.Hour}, 21 | {"In 5 seconde moet een auto verplaatsen", 0, "In 5 seconde", 5 * time.Second}, 22 | {"binnen twee weken", 0, "binnen twee weken", 14 * 24 * time.Hour}, 23 | {"binnen een maand", 0, "binnen een maand", 31 * 24 * time.Hour}, 24 | {"na een maand", 0, "na een maand", 31 * 24 * time.Hour}, 25 | {"binnen een paar maanden", 0, "binnen een paar maanden", 91 * 24 * time.Hour}, 26 | {"binnen een jaar", 0, "binnen een jaar", 366 * 24 * time.Hour}, 27 | {"in een week", 0, "in een week", 7 * 24 * time.Hour}, 28 | } 29 | 30 | w := when.New(nil) 31 | w.Add(nl.Deadline(rules.Skip)) 32 | 33 | ApplyFixtures(t, "nl.Deadline", w, fixt) 34 | } 35 | -------------------------------------------------------------------------------- /rules/nl/exact_month_date.go: -------------------------------------------------------------------------------- 1 | package nl 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | // <[]string{"derde van maart", "derde", "", "maart", "", ""}> 13 | // <[]string{"3e van march", "3e", "", "maart", "", ""}> 14 | // <[]string{"1e van september", "1e", "", "september", "", ""}> 15 | // <[]string{"1 sept.", "", "", "1", "sept", ""}> 16 | // <[]string{"twintigste van december", "twintigste", "", "december", "", ""}> 17 | // <[]string{"februari", "", "", "februari", "", ""}> 18 | // <[]string{"oktober", "", "", "oktober", "", ""}> 19 | // <[]string{"jul.", "", "", "jul.", "", ""}> 20 | // <[]string{"juni", "", "", "juni", "", ""}> 21 | 22 | // https://play.golang.org/p/Zfjl6ERNkq 23 | 24 | // 1. - ordinal day? 25 | // 2. - numeric day? 26 | // 3. - month 27 | // 4. - ordinal day? 28 | // 5. - ordinal day? 29 | 30 | func ExactMonthDate(s rules.Strategy) rules.Rule { 31 | overwrite := s == rules.Override 32 | 33 | return &rules.F{ 34 | RegExp: regexp.MustCompile("(?i)" + 35 | "(?:\\W|^)" + 36 | "(?:(?:(" + ORDINAL_WORDS_PATTERN[3:] + "(?:\\s+van)?|([0-9]+))\\s*)?" + 37 | "(" + MONTH_OFFSET_PATTERN[3:] + // skip '(?:' 38 | "(?:\\s*(?:(" + ORDINAL_WORDS_PATTERN[3:] + "|([0-9]+)))?" + 39 | "(?:\\W|$)", 40 | ), 41 | 42 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 43 | _ = overwrite 44 | 45 | ord1 := strings.ToLower(strings.TrimSpace(m.Captures[0])) 46 | num1 := strings.ToLower(strings.TrimSpace(m.Captures[1])) 47 | mon := strings.ToLower(strings.TrimSpace(m.Captures[2])) 48 | ord2 := strings.ToLower(strings.TrimSpace(m.Captures[3])) 49 | num2 := strings.ToLower(strings.TrimSpace(m.Captures[4])) 50 | 51 | monInt, ok := MONTH_OFFSET[mon] 52 | if !ok { 53 | return false, nil 54 | } 55 | 56 | c.Month = &monInt 57 | 58 | if ord1 != "" { 59 | ordInt, ok := ORDINAL_WORDS[ord1] 60 | if !ok { 61 | return false, nil 62 | } 63 | 64 | c.Day = &ordInt 65 | } 66 | 67 | if num1 != "" { 68 | n, err := strconv.ParseInt(num1, 10, 8) 69 | if err != nil { 70 | return false, nil 71 | } 72 | 73 | num := int(n) 74 | 75 | c.Day = &num 76 | } 77 | 78 | if ord2 != "" { 79 | ordInt, ok := ORDINAL_WORDS[ord2] 80 | if !ok { 81 | return false, nil 82 | } 83 | 84 | c.Day = &ordInt 85 | } 86 | 87 | if num2 != "" { 88 | n, err := strconv.ParseInt(num2, 10, 8) 89 | if err != nil { 90 | return false, nil 91 | } 92 | 93 | num := int(n) 94 | 95 | c.Day = &num 96 | } 97 | 98 | return true, nil 99 | }, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /rules/nl/exact_month_date_test.go: -------------------------------------------------------------------------------- 1 | package nl_test 2 | 3 | import ( 4 | "github.com/olebedev/when/rules/nl" 5 | "testing" 6 | "time" 7 | 8 | "github.com/olebedev/when" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func TestExactMonthDate(t *testing.T) { 13 | w := when.New(nil) 14 | w.Add(nl.ExactMonthDate(rules.Override)) 15 | 16 | fixtok := []Fixture{ 17 | {"derde van maart", 0, "derde van maart", 1368 * time.Hour}, 18 | {"3e van maart", 0, "3e van maart", 1368 * time.Hour}, 19 | {"1 september", 0, "1 september", 5736 * time.Hour}, 20 | {"1 sept", 0, "1 sept", 5736 * time.Hour}, 21 | {"1 sept.", 0, "1 sept.", 5736 * time.Hour}, 22 | {"1e van september", 0, "1e van september", 5736 * time.Hour}, 23 | {"twintigste van december", 0, "twintigste van december", 8376 * time.Hour}, 24 | {"februari", 0, "februari", 744 * time.Hour}, 25 | {"oktober", 0, "oktober", 6576 * time.Hour}, 26 | {"jul.", 0, "jul.", 4368 * time.Hour}, 27 | {"juni", 0, "juni", 3648 * time.Hour}, 28 | } 29 | 30 | ApplyFixtures(t, "nl.ExactMonthDate", w, fixtok) 31 | } 32 | -------------------------------------------------------------------------------- /rules/nl/hour.go: -------------------------------------------------------------------------------- 1 | package nl 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/pkg/errors" 10 | 11 | "github.com/olebedev/when/rules" 12 | ) 13 | 14 | /* 15 | "5u" 16 | "5 uur" 17 | "5am" 18 | "5pm" 19 | "5A." 20 | "5P." 21 | "11 P.M." 22 | https://play.golang.org/p/2Gh35Sl3KP 23 | */ 24 | 25 | func Hour(s rules.Strategy) rules.Rule { 26 | return &rules.F{ 27 | RegExp: regexp.MustCompile("(?i)(?:\\W|^)" + 28 | "(?:\\s*((om)?))" + 29 | "(\\d{1,2})" + 30 | "(?:\\s*(U\\.?|UUR|A\\.|P\\.|A\\.M\\.|P\\.M\\.|AM?|PM?))" + 31 | "(?:\\s*((in de|\\'s) (middags?|avonds?))?)" + 32 | "(?:\\W|$)"), 33 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 34 | if c.Hour != nil && s != rules.Override { 35 | return false, nil 36 | } 37 | 38 | lower := strings.ToLower(strings.TrimSpace(m.String())) 39 | hour, err := strconv.Atoi(m.Captures[2]) 40 | 41 | if err != nil { 42 | return false, errors.Wrap(err, "hour rule") 43 | } 44 | 45 | zero := 0 46 | 47 | if hour > 23 { 48 | return false, nil 49 | } 50 | c.Hour = &hour 51 | 52 | // pm 53 | if regexp.MustCompile("p.?(m.?)?").MatchString(strings.ToLower(strings.TrimSpace(m.Captures[3]))) { 54 | if hour < 12 { 55 | hour += 12 56 | } 57 | 58 | c.Hour = &hour 59 | } 60 | 61 | // afternoon or evening 62 | if (strings.Contains(lower, "middag") || strings.Contains(lower, "avond")) && hour < 12 { 63 | hour += 12 64 | c.Hour = &hour 65 | } 66 | 67 | c.Minute = &zero 68 | c.Second = &zero 69 | return true, nil 70 | }, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rules/nl/hour_minute.go: -------------------------------------------------------------------------------- 1 | package nl 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/olebedev/when/rules" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | /* 14 | {"17:30", 0, "17:30", 0}, 15 | {"17:30u", 0, "17:30u", 0}, 16 | {"om 17:30 uur", 3, "17:30 uur", 0}, 17 | {"om 5:59 pm", 3, "5:59 pm", 0}, 18 | 19 | https://play.golang.org/p/hXl7C8MWNr 20 | */ 21 | 22 | // 1. - at? 23 | // 2. - int 24 | // 3. - int 25 | // 4. - ext? 26 | // 5. - day part? 27 | 28 | func HourMinute(s rules.Strategy) rules.Rule { 29 | return &rules.F{ 30 | RegExp: regexp.MustCompile("(?i)(?:\\W|^)" + 31 | "(?:\\s*((om)?))" + 32 | "((?:[0-1]{0,1}[0-9])|(?:2[0-3]))" + 33 | "(?:\\:|:)" + 34 | "((?:[0-5][0-9]))" + 35 | "(?:\\s*(U\\.?|UUR|A\\.|P\\.|A\\.M\\.|P\\.M\\.|AM?|PM?))?" + 36 | "(?:\\s*((in de|\\'s) (middags?|avonds?))?)" + 37 | "(?:\\W|$)"), 38 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 39 | if (c.Hour != nil || c.Minute != nil) && s != rules.Override { 40 | return false, nil 41 | } 42 | 43 | lower := strings.ToLower(strings.TrimSpace(m.String())) 44 | hour, err := strconv.Atoi(m.Captures[2]) 45 | if err != nil { 46 | return false, errors.Wrap(err, "hour minute rule") 47 | } 48 | 49 | minutes, err := strconv.Atoi(m.Captures[3]) 50 | if err != nil { 51 | return false, errors.Wrap(err, "hour minute rule") 52 | } 53 | 54 | if minutes > 59 { 55 | return false, nil 56 | } 57 | c.Minute = &minutes 58 | 59 | if hour > 23 { 60 | return false, nil 61 | } 62 | c.Hour = &hour 63 | 64 | // pm 65 | if regexp.MustCompile("p.?(m.?)?").MatchString(strings.ToLower(strings.TrimSpace(m.Captures[4]))) { 66 | if hour < 12 { 67 | hour += 12 68 | } 69 | 70 | c.Hour = &hour 71 | } 72 | 73 | // afternoon or evening 74 | if (strings.Contains(lower, "middag") || strings.Contains(lower, "avond")) && hour < 12 { 75 | hour += 12 76 | c.Hour = &hour 77 | } 78 | 79 | seconds := 0 // Truncate seconds 80 | c.Second = &seconds 81 | 82 | return true, nil 83 | }, 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rules/nl/hour_minute_test.go: -------------------------------------------------------------------------------- 1 | package nl_test 2 | 3 | import ( 4 | "github.com/olebedev/when/rules/nl" 5 | "testing" 6 | "time" 7 | 8 | "github.com/olebedev/when" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func TestHourMinute(t *testing.T) { 13 | w := when.New(nil) 14 | w.Add(nl.HourMinute(rules.Override)) 15 | 16 | fixtok := []Fixture{ 17 | {"17:30u", 0, "17:30u", (17 * time.Hour) + (30 * time.Minute)}, 18 | {"om 17:30 uur", 3, "17:30 uur", (17 * time.Hour) + (30 * time.Minute)}, 19 | {"om 5:59 pm", 3, "5:59 pm", (17 * time.Hour) + (59 * time.Minute)}, 20 | {"om 5:59 am", 3, "5:59 am", (5 * time.Hour) + (59 * time.Minute)}, 21 | } 22 | 23 | fixtnil := []Fixture{ 24 | {"28:30pm", 0, "", 0}, 25 | {"12:61u", 0, "", 0}, 26 | {"24:10", 0, "", 0}, 27 | } 28 | 29 | ApplyFixtures(t, "nl.HourMinute", w, fixtok) 30 | ApplyFixturesNil(t, "on.HourMinute nil", w, fixtnil) 31 | 32 | w.Add(nl.Hour(rules.Skip)) 33 | ApplyFixtures(t, "nl.HourMinute|nl.Hour", w, fixtok) 34 | ApplyFixturesNil(t, "on.HourMinute|nl.Hour nil", w, fixtnil) 35 | 36 | w = when.New(nil) 37 | w.Add( 38 | nl.Hour(rules.Override), 39 | nl.HourMinute(rules.Override), 40 | ) 41 | 42 | ApplyFixtures(t, "nl.Hour|nl.HourMinute", w, fixtok) 43 | ApplyFixturesNil(t, "on.Hour|nl.HourMinute nil", w, fixtnil) 44 | } 45 | -------------------------------------------------------------------------------- /rules/nl/hour_test.go: -------------------------------------------------------------------------------- 1 | package nl_test 2 | 3 | import ( 4 | "github.com/olebedev/when" 5 | "github.com/olebedev/when/rules" 6 | "github.com/olebedev/when/rules/nl" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestHour(t *testing.T) { 12 | fixt := []Fixture{ 13 | {"5pm", 0, "5pm", 17 * time.Hour}, 14 | {"5 uur in de avond", 0, "5 uur in de avond", 17 * time.Hour}, 15 | {"5 uur 's avonds", 0, "5 uur 's avonds", 17 * time.Hour}, 16 | {"om 17 uur", 3, "17 uur", 17 * time.Hour}, 17 | {"om 5 P.", 3, "5 P.", 17 * time.Hour}, 18 | {"om 12 P.", 3, "12 P.", 12 * time.Hour}, 19 | {"om 1 P.", 3, "1 P.", 13 * time.Hour}, 20 | {"om 5 am", 3, "5 am", 5 * time.Hour}, 21 | {"om 5A", 3, "5A", 5 * time.Hour}, 22 | {"om 5A.", 3, "5A.", 5 * time.Hour}, 23 | {"5A.", 0, "5A.", 5 * time.Hour}, 24 | {"11 P.M.", 0, "11 P.M.", 23 * time.Hour}, 25 | } 26 | 27 | w := when.New(nil) 28 | w.Add(nl.Hour(rules.Override)) 29 | 30 | ApplyFixtures(t, "nl.Hour", w, fixt) 31 | } 32 | -------------------------------------------------------------------------------- /rules/nl/nl.go: -------------------------------------------------------------------------------- 1 | package nl 2 | 3 | import "github.com/olebedev/when/rules" 4 | 5 | var All = []rules.Rule{ 6 | Weekday(rules.Override), 7 | CasualDate(rules.Override), 8 | CasualTime(rules.Override), 9 | Hour(rules.Override), 10 | HourMinute(rules.Override), 11 | Deadline(rules.Override), 12 | PastTime(rules.Override), 13 | ExactMonthDate(rules.Override), 14 | } 15 | 16 | var WEEKDAY_OFFSET = map[string]int{ 17 | "zondag": 0, 18 | "zon": 0, 19 | "zo": 0, 20 | "maandag": 1, 21 | "maa": 1, 22 | "ma": 1, 23 | "dinsdag": 2, 24 | "din": 2, 25 | "di": 2, 26 | "woensdag": 3, 27 | "woe": 3, 28 | "wo": 3, 29 | "donderdag": 4, 30 | "don": 4, 31 | "do": 4, 32 | "vrijdag": 5, 33 | "vrij": 5, 34 | "vr": 5, 35 | "zaterdag": 6, 36 | "zat": 6, 37 | "za": 6, 38 | } 39 | 40 | var WEEKDAY_OFFSET_PATTERN = "(?:zondag|zon|zo|maandag|maa|ma|dinsdag|din|di|woensdag|woe|wo|donderdag|don|do|vrijdag|vrij|vr|zaterdag|zat|za)" 41 | 42 | var MONTH_OFFSET = map[string]int{ 43 | "january": 1, 44 | "jan": 1, 45 | "jan.": 1, 46 | "februari": 2, 47 | "feb": 2, 48 | "feb.": 2, 49 | "maart": 3, 50 | "mrt": 3, 51 | "mrt.": 3, 52 | "april": 4, 53 | "apr": 4, 54 | "apr.": 4, 55 | "mei": 5, 56 | "juni": 6, 57 | "jun": 6, 58 | "jun.": 6, 59 | "juli": 7, 60 | "jul": 7, 61 | "jul.": 7, 62 | "augustus": 8, 63 | "aug": 8, 64 | "aug.": 8, 65 | "september": 9, 66 | "sep": 9, 67 | "sep.": 9, 68 | "sept": 9, 69 | "sept.": 9, 70 | "oktober": 10, 71 | "okt": 10, 72 | "okt.": 10, 73 | "november": 11, 74 | "nov": 11, 75 | "nov.": 11, 76 | "december": 12, 77 | "dec": 12, 78 | "dec.": 12, 79 | } 80 | 81 | var MONTH_OFFSET_PATTERN = `(?:january|jan\.?|februari|feb\.?|maart|mrt\.?|april|apr\.?|mei|juni|jun\.?|juli|jul\.?|augustus|aug\.?|september|sept?\.?|oktober|okt\.?|november|nov\.?|december|dec\.?)` 82 | 83 | var INTEGER_WORDS = map[string]int{ 84 | "een": 1, 85 | "één": 1, 86 | "twee": 2, 87 | "drie": 3, 88 | "vier": 4, 89 | "vijf": 5, 90 | "zes": 6, 91 | "zeven": 7, 92 | "acht": 8, 93 | "negen": 9, 94 | "tien": 10, 95 | "elf": 11, 96 | "twaalf": 12, 97 | } 98 | 99 | var INTEGER_WORDS_PATTERN = `(?:een|één|twee|drie|vier|vijf|zes|zeven|acht|negen|tien|elf|twaalf)` 100 | 101 | var ORDINAL_WORDS = map[string]int{ 102 | "eerste": 1, 103 | "1e": 1, 104 | "tweede": 2, 105 | "2e": 2, 106 | "derde": 3, 107 | "3e": 3, 108 | "vierde": 4, 109 | "4e": 4, 110 | "vijfde": 5, 111 | "5e": 5, 112 | "zesde": 6, 113 | "6e": 6, 114 | "zevende": 7, 115 | "7e": 7, 116 | "achtste": 8, 117 | "8e": 8, 118 | "negende": 9, 119 | "9e": 9, 120 | "tiende": 10, 121 | "10e": 10, 122 | "elfde": 11, 123 | "11e": 11, 124 | "twaalfde": 12, 125 | "12e": 12, 126 | "derdiende": 13, 127 | "13e": 13, 128 | "veertiende": 14, 129 | "14e": 14, 130 | "vijftiende": 15, 131 | "15e": 15, 132 | "zestiende": 16, 133 | "16e": 16, 134 | "zeventiende": 17, 135 | "17e": 17, 136 | "achttiende": 18, 137 | "18e": 18, 138 | "negentiende": 19, 139 | "19e": 19, 140 | "twintigste": 20, 141 | "20e": 20, 142 | "eenentwintigste": 21, 143 | "21e": 21, 144 | "tweeentwintigste": 22, 145 | "22e": 22, 146 | "drieentwintigste": 23, 147 | "23e": 23, 148 | "vierentwintigste": 24, 149 | "24e": 24, 150 | "vijfentwintigste": 25, 151 | "25e": 25, 152 | "zesentwintigste": 26, 153 | "26e": 26, 154 | "zevenentwintigste": 27, 155 | "27e": 27, 156 | "achtentwintigste": 28, 157 | "28e": 28, 158 | "negenentwintigste": 29, 159 | "29e": 29, 160 | "dertigste": 30, 161 | "30e": 30, 162 | "eenendertigste": 31, 163 | "31e": 31, 164 | } 165 | 166 | var ORDINAL_WORDS_PATTERN = `(?:1e|eerste|2e|tweede|3e|derde|4e|vierde|5e|vijfde|6e|zesde|7e|zevende|8e|achtste|9e|negende|10e|tiende|11e|elfde|12e|twaalfde|13e|derdiende|14e|veertiende|15e|vijftiende|16e|zestiende|17e|zeventiende|18e|achttiende|19e|negentiende|20e|twintigste|21e|eenentwintigste|22e|tweeentwintigste|23e|drieentwintigste|24e|vierentwintigste|25e|vijfentwintigste|26e|zesentwintigste|27e|zevenentwintigste|28e|achtentwintigste|29e|negenentwintigste|30e|dertigste|31e|eenendertigste)` 167 | -------------------------------------------------------------------------------- /rules/nl/nl_test.go: -------------------------------------------------------------------------------- 1 | package nl_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when/rules/nl" 8 | 9 | "github.com/olebedev/when" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | var null = time.Date(2016, time.January, 6, 0, 0, 0, 0, time.UTC) 14 | 15 | type Fixture struct { 16 | Text string 17 | Index int 18 | Phrase string 19 | Diff time.Duration 20 | } 21 | 22 | func ApplyFixtures(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 23 | for i, f := range fixt { 24 | res, err := w.Parse(f.Text, null) 25 | require.Nil(t, err, "[%s] err #%d", name, i) 26 | require.NotNil(t, res, "[%s] res #%d", name, i) 27 | require.Equal(t, f.Index, res.Index, "[%s] index #%d", name, i) 28 | require.Equal(t, f.Phrase, res.Text, "[%s] text #%d", name, i) 29 | require.Equal(t, f.Diff, res.Time.Sub(null), "[%s] diff #%d", name, i) 30 | } 31 | } 32 | 33 | func ApplyFixturesNil(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 34 | for i, f := range fixt { 35 | res, err := w.Parse(f.Text, null) 36 | require.Nil(t, err, "[%s] err #%d", name, i) 37 | require.Nil(t, res, "[%s] res #%d", name, i) 38 | } 39 | } 40 | 41 | func ApplyFixturesErr(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 42 | for i, f := range fixt { 43 | _, err := w.Parse(f.Text, null) 44 | require.NotNil(t, err, "[%s] err #%d", name, i) 45 | require.Equal(t, f.Phrase, err.Error(), "[%s] err text #%d", name, i) 46 | } 47 | } 48 | 49 | func TestAll(t *testing.T) { 50 | w := when.New(nil) 51 | w.Add(nl.All...) 52 | 53 | // complex cases 54 | fixt := []Fixture{ 55 | {"vorige week zondag om 10:00", 0, "vorige week zondag om 10:00", ((-3 * 24) + 10) * time.Hour}, 56 | {"vanavond om 23:10", 0, "vanavond om 23:10", (23 * time.Hour) + (10 * time.Minute)}, 57 | {"op vrijdagmiddag", 3, "vrijdagmiddag", ((2 * 24) + 15) * time.Hour}, 58 | {"komende dinsdag om 14:00", 0, "komende dinsdag om 14:00", ((6 * 24) + 14) * time.Hour}, 59 | {"komende dinsdag 2 uur 's middags", 0, "komende dinsdag 2 uur 's middags", ((6 * 24) + 14) * time.Hour}, 60 | {"komende woensdag om 14:25", 0, "komende woensdag om 14:25", (((7 * 24) + 14) * time.Hour) + (25 * time.Minute)}, 61 | {"om 11 uur afgelopen dinsdag", 3, "11 uur afgelopen dinsdag", -13 * time.Hour}, 62 | {"volgende week dinsdag om 18:15", 0, "volgende week dinsdag om 18:15", (((6 * 24) + 18) * time.Hour) + (15 * time.Minute)}, 63 | {"volgende week vrijdag", 0, "volgende week vrijdag", (9 * 24) * time.Hour}, 64 | } 65 | 66 | ApplyFixtures(t, "nl.All...", w, fixt) 67 | } 68 | -------------------------------------------------------------------------------- /rules/nl/past_time.go: -------------------------------------------------------------------------------- 1 | package nl 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/AlekSi/pointer" 10 | "github.com/olebedev/when/rules" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func PastTime(s rules.Strategy) rules.Rule { 15 | overwrite := s == rules.Override 16 | 17 | return &rules.F{ 18 | RegExp: regexp.MustCompile( 19 | "(?i)(?:\\W|^)\\s*" + 20 | "(" + INTEGER_WORDS_PATTERN + "|[0-9]+|een(?:\\s*(paar|half|halve))?)\\s*" + 21 | "(seconden?|minuut|minuten|uur|uren|dag|dagen|week|weken|maand|maanden|jaar|jaren) (geleden)\\s*" + 22 | "(?:\\W|$)"), 23 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 24 | 25 | numStr := strings.TrimSpace(m.Captures[0]) 26 | 27 | var num int 28 | var err error 29 | 30 | if n, ok := INTEGER_WORDS[numStr]; ok { 31 | num = n 32 | } else if numStr == "een" { 33 | num = 1 34 | } else if strings.Contains(numStr, "paar") { 35 | num = 3 36 | } else if strings.Contains(numStr, "half") || strings.Contains(numStr, "halve") { 37 | // pass 38 | } else { 39 | num, err = strconv.Atoi(numStr) 40 | if err != nil { 41 | return false, errors.Wrapf(err, "convert '%s' to int", numStr) 42 | } 43 | } 44 | 45 | exponent := strings.TrimSpace(m.Captures[2]) 46 | 47 | if !strings.Contains(numStr, "half") && !strings.Contains(numStr, "halve") { 48 | switch { 49 | case strings.Contains(exponent, "seconde"): 50 | if c.Duration == 0 || overwrite { 51 | c.Duration = -(time.Duration(num) * time.Second) 52 | } 53 | case strings.Contains(exponent, "min"): 54 | if c.Duration == 0 || overwrite { 55 | c.Duration = -(time.Duration(num) * time.Minute) 56 | } 57 | case strings.Contains(exponent, "uur"), strings.Contains(exponent, "uren"): 58 | if c.Duration == 0 || overwrite { 59 | c.Duration = -(time.Duration(num) * time.Hour) 60 | } 61 | case strings.Contains(exponent, "dag"): 62 | if c.Duration == 0 || overwrite { 63 | c.Duration = -(time.Duration(num) * 24 * time.Hour) 64 | } 65 | case strings.Contains(exponent, "week"), strings.Contains(exponent, "weken"): 66 | if c.Duration == 0 || overwrite { 67 | c.Duration = -(time.Duration(num) * 7 * 24 * time.Hour) 68 | } 69 | case strings.Contains(exponent, "maand"): 70 | if c.Month == nil || overwrite { 71 | c.Month = pointer.ToInt((int(ref.Month()) - num) % 12) 72 | } 73 | case strings.Contains(exponent, "jaar"): 74 | if c.Year == nil || overwrite { 75 | c.Year = pointer.ToInt(ref.Year() - num) 76 | } 77 | } 78 | } else { 79 | switch { 80 | case strings.Contains(exponent, "uur"), strings.Contains(exponent, "uren"): 81 | if c.Duration == 0 || overwrite { 82 | c.Duration = -(30 * time.Minute) 83 | } 84 | case strings.Contains(exponent, "dag"): 85 | if c.Duration == 0 || overwrite { 86 | c.Duration = -(12 * time.Hour) 87 | } 88 | case strings.Contains(exponent, "week"): 89 | if c.Duration == 0 || overwrite { 90 | c.Duration = -(7 * 12 * time.Hour) 91 | } 92 | case strings.Contains(exponent, "maand"): 93 | if c.Duration == 0 || overwrite { 94 | // 2 weeks 95 | c.Duration = -(14 * 24 * time.Hour) 96 | } 97 | case strings.Contains(exponent, "jaar"): 98 | if c.Month == nil || overwrite { 99 | c.Month = pointer.ToInt((int(ref.Month()) - 6) % 12) 100 | } 101 | } 102 | } 103 | 104 | return true, nil 105 | }, 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /rules/nl/past_time_test.go: -------------------------------------------------------------------------------- 1 | package nl_test 2 | 3 | import ( 4 | "github.com/olebedev/when/rules/nl" 5 | "testing" 6 | "time" 7 | 8 | "github.com/olebedev/when" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func TestPastTime(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"een half uur geleden", 0, "een half uur geleden", -(time.Hour / 2)}, 15 | {"1 uur geleden", 0, "1 uur geleden", -(time.Hour)}, 16 | {"5 minuten geleden", 0, "5 minuten geleden", -(time.Minute * 5)}, 17 | {"5 minuten geleden ging ik naar de dierentuin", 0, "5 minuten geleden", -(time.Minute * 5)}, 18 | {"we deden iets 10 dagen geleden", 14, "10 dagen geleden", -(10 * 24 * time.Hour)}, 19 | {"we deden iets vijf dagen geleden", 14, "vijf dagen geleden", -(5 * 24 * time.Hour)}, 20 | {"we deden iets 5 dagen geleden", 14, "5 dagen geleden", -(5 * 24 * time.Hour)}, 21 | {"5 seconden geleden werd een auto weggesleept", 0, "5 seconden geleden", -(5 * time.Second)}, 22 | {"twee weken geleden", 0, "twee weken geleden", -(14 * 24 * time.Hour)}, 23 | {"een maand geleden", 0, "een maand geleden", -(31 * 24 * time.Hour)}, 24 | {"een paar maanden geleden", 0, "een paar maanden geleden", -(92 * 24 * time.Hour)}, 25 | {"een jaar geleden", 0, "een jaar geleden", -(365 * 24 * time.Hour)}, 26 | {"een week geleden", 0, "een week geleden", -(7 * 24 * time.Hour)}, 27 | } 28 | 29 | w := when.New(nil) 30 | w.Add(nl.PastTime(rules.Skip)) 31 | 32 | ApplyFixtures(t, "nl.PastTime", w, fixt) 33 | } 34 | -------------------------------------------------------------------------------- /rules/nl/weekday.go: -------------------------------------------------------------------------------- 1 | package nl 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | ) 10 | 11 | func Weekday(s rules.Strategy) rules.Rule { 12 | overwrite := s == rules.Override 13 | 14 | return &rules.F{ 15 | RegExp: regexp.MustCompile("(?i)" + 16 | "(?:\\W|^)" + 17 | "(?:op\\s*?)?" + 18 | "(?:(deze|vorige|vorige week|afgelopen|volgende|volgende week|komende|komende week)\\s*)?" + 19 | "(" + WEEKDAY_OFFSET_PATTERN[3:] + // skip '(?:' 20 | "(?:\\s*(deze|vorige|afgelopen|volgende|komende)\\s*week)?" + 21 | "(?:\\W|$)", 22 | ), 23 | 24 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 25 | _ = overwrite 26 | 27 | day := strings.ToLower(strings.TrimSpace(m.Captures[1])) 28 | norm := strings.ToLower(strings.TrimSpace(m.Captures[0] + m.Captures[2])) 29 | if norm == "" { 30 | norm = "volgende" 31 | } 32 | dayInt, ok := WEEKDAY_OFFSET[day] 33 | if !ok { 34 | return false, nil 35 | } 36 | 37 | if c.Duration != 0 && !overwrite { 38 | return false, nil 39 | } 40 | 41 | // Switch: 42 | switch { 43 | case strings.Contains(norm, "vorige week"): 44 | if dayInt == 6 { 45 | dayInt = -1 46 | } 47 | diff := int(ref.Weekday()) - dayInt 48 | if diff != 0 && dayInt <= 0 { 49 | c.Duration = -time.Duration(diff) * 24 * time.Hour 50 | } else if diff != 0 { 51 | c.Duration = -time.Duration(7+diff) * 24 * time.Hour 52 | } else { 53 | c.Duration = -(7 * 24 * time.Hour) 54 | } 55 | case strings.Contains(norm, "afgelopen") || strings.Contains(norm, "vorige"): 56 | diff := int(ref.Weekday()) - dayInt 57 | if diff > 0 { 58 | c.Duration = -time.Duration(diff*24) * time.Hour 59 | } else if diff < 0 { 60 | c.Duration = -time.Duration(7+diff) * 24 * time.Hour 61 | } else { 62 | c.Duration = -(7 * 24 * time.Hour) 63 | } 64 | case strings.Contains(norm, "volgende week"): 65 | if dayInt == 0 { 66 | dayInt = 7 67 | } 68 | diff := dayInt - int(ref.Weekday()) 69 | c.Duration = time.Duration(7+diff) * 24 * time.Hour 70 | case strings.Contains(norm, "volgende"), strings.Contains(norm, "komende"): 71 | diff := dayInt - int(ref.Weekday()) 72 | if diff > 0 { 73 | c.Duration = time.Duration(diff) * 24 * time.Hour 74 | } else if diff < 0 { 75 | c.Duration = time.Duration(7+diff) * 24 * time.Hour 76 | } else { 77 | c.Duration = 7 * 24 * time.Hour 78 | } 79 | case strings.Contains(norm, "deze"): 80 | if int(ref.Weekday()) < dayInt { 81 | diff := dayInt - int(ref.Weekday()) 82 | if diff > 0 { 83 | c.Duration = time.Duration(diff) * 24 * time.Hour 84 | } else if diff < 0 { 85 | c.Duration = time.Duration(7+diff) * 24 * time.Hour 86 | } else { 87 | c.Duration = 7 * 24 * time.Hour 88 | } 89 | } else if int(ref.Weekday()) > dayInt { 90 | diff := int(ref.Weekday()) - dayInt 91 | if diff > 0 { 92 | c.Duration = -time.Duration(diff) * 24 * time.Hour 93 | } else if diff < 0 { 94 | c.Duration = -time.Duration(7+diff) * 24 * time.Hour 95 | } else { 96 | c.Duration = -(7 * 24 * time.Hour) 97 | } 98 | } 99 | } 100 | 101 | return true, nil 102 | }, 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /rules/nl/weekday_test.go: -------------------------------------------------------------------------------- 1 | package nl_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when/rules/nl" 8 | 9 | "github.com/olebedev/when" 10 | "github.com/olebedev/when/rules" 11 | ) 12 | 13 | func TestWeekday(t *testing.T) { 14 | // current is Wednesday 15 | fixt := []Fixture{ 16 | // past week 17 | {"vorige week maandag", 0, "vorige week maandag", -(9 * 24 * time.Hour)}, 18 | {"vorige week dinsdag", 0, "vorige week dinsdag", -(8 * 24 * time.Hour)}, 19 | {"vorige week woensdag", 0, "vorige week woensdag", -(7 * 24 * time.Hour)}, 20 | {"vorige week donderdag", 0, "vorige week donderdag", -(6 * 24 * time.Hour)}, 21 | {"vorige week vrijdag", 0, "vorige week vrijdag", -(5 * 24 * time.Hour)}, 22 | {"vorige week zaterdag", 0, "vorige week zaterdag", -(4 * 24 * time.Hour)}, 23 | {"vorige week zondag", 0, "vorige week zondag", -(3 * 24 * time.Hour)}, 24 | // past/last 25 | {"doe het voor afgelopen maandag", 13, "afgelopen maandag", -(2 * 24 * time.Hour)}, 26 | {"afgelopen zaterdag", 0, "afgelopen zaterdag", -(4 * 24 * time.Hour)}, 27 | {"afgelopen vrijdag", 0, "afgelopen vrijdag", -(5 * 24 * time.Hour)}, 28 | {"afgelopen woensdag", 0, "afgelopen woensdag", -(7 * 24 * time.Hour)}, 29 | {"afgelopen dinsdag", 0, "afgelopen dinsdag", -(24 * time.Hour)}, 30 | // next week 31 | {"volgende week maandag", 0, "volgende week maandag", 5 * 24 * time.Hour}, 32 | {"volgende week dinsdag", 0, "volgende week dinsdag", 6 * 24 * time.Hour}, 33 | {"volgende week woensdag", 0, "volgende week woensdag", 7 * 24 * time.Hour}, 34 | {"volgende week donderdag", 0, "volgende week donderdag", 8 * 24 * time.Hour}, 35 | {"volgende week vrijdag", 0, "volgende week vrijdag", 9 * 24 * time.Hour}, 36 | {"volgende week zaterdag", 0, "volgende week zaterdag", 10 * 24 * time.Hour}, 37 | {"volgende week zondag", 0, "volgende week zondag", 11 * 24 * time.Hour}, 38 | // next 39 | {"komende dinsdag", 0, "komende dinsdag", 6 * 24 * time.Hour}, 40 | {"stuur me een bericht komende woensdag", 21, "komende woensdag", 7 * 24 * time.Hour}, 41 | {"komende zaterdag", 0, "komende zaterdag", 3 * 24 * time.Hour}, 42 | {"volgende dinsdag", 0, "volgende dinsdag", 6 * 24 * time.Hour}, 43 | {"stuur me een bericht volgende woensdag", 21, "volgende woensdag", 7 * 24 * time.Hour}, 44 | {"volgende zaterdag", 0, "volgende zaterdag", 3 * 24 * time.Hour}, 45 | // this 46 | {"deze dinsdag", 0, "deze dinsdag", -(24 * time.Hour)}, 47 | {"stuur me een bericht deze woensdag", 21, "deze woensdag", 0}, 48 | {"deze zaterdag", 0, "deze zaterdag", 3 * 24 * time.Hour}, 49 | } 50 | 51 | w := when.New(nil) 52 | 53 | w.Add(nl.Weekday(rules.Override)) 54 | 55 | ApplyFixtures(t, "nl.Weekday", w, fixt) 56 | } 57 | -------------------------------------------------------------------------------- /rules/ru/casual_date.go: -------------------------------------------------------------------------------- 1 | package ru 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | ) 10 | 11 | // https://play.golang.org/p/QrFtjmjUoJ 12 | 13 | func CasualDate(s rules.Strategy) rules.Rule { 14 | return &rules.F{ 15 | RegExp: regexp.MustCompile("(?i)(?:\\P{L}|^)" + 16 | "((?:до|прямо)\\s+)?" + 17 | "(сейчас|сегодня|завтра|вчера)" + 18 | "(?:\\P{L}|$)"), 19 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 20 | lower := strings.ToLower(strings.TrimSpace(m.String())) 21 | 22 | switch { 23 | case strings.Contains(lower, "сегодня"): 24 | // c.Hour = pointer.ToInt(18) 25 | case strings.Contains(lower, "завтра"): 26 | if c.Duration == 0 || s == rules.Override { 27 | c.Duration += time.Hour * 24 28 | } 29 | case strings.Contains(lower, "вчера"): 30 | if c.Duration == 0 || s == rules.Override { 31 | c.Duration -= time.Hour * 24 32 | } 33 | } 34 | 35 | return true, nil 36 | }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rules/ru/casual_test.go: -------------------------------------------------------------------------------- 1 | package ru_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/ru" 10 | ) 11 | 12 | func TestCasualDate(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"Это нужно сделать прямо сейчас", 33, "прямо сейчас", 0}, 15 | {"Это нужно сделать сегодня", 33, "сегодня", 0}, 16 | {"Это нужно сделать завтра вечером", 33, "завтра", time.Hour * 24}, 17 | {"Это нужно было сделать вчера вечером", 42, "вчера", -(time.Hour * 24)}, 18 | {"Это нужно сделать до завтра", 33, "до завтра", time.Hour * 24}, 19 | } 20 | 21 | w := when.New(nil) 22 | w.Add(ru.CasualDate(rules.Skip)) 23 | 24 | ApplyFixtures(t, "ru.CasualDate", w, fixt) 25 | } 26 | 27 | func TestCasualTime(t *testing.T) { 28 | fixt := []Fixture{ 29 | {"Это нужно было сделать этим утром ", 42, "этим утром", 8 * time.Hour}, 30 | {"Это нужно сделать до обеда", 33, "до обеда", 12 * time.Hour}, 31 | {"Это нужно сделать после обеда", 33, "после обеда", 15 * time.Hour}, 32 | {"Это нужно сделать к вечеру", 33, "к вечеру", 18 * time.Hour}, 33 | {"вечером", 0, "вечером", 18 * time.Hour}, 34 | {"вечером", 0, "вечером", 18 * time.Hour}, 35 | } 36 | 37 | w := when.New(nil) 38 | w.Add(ru.CasualTime(rules.Skip)) 39 | 40 | ApplyFixtures(t, "ru.CasualTime", w, fixt) 41 | } 42 | 43 | func TestCasualDateCasualTime(t *testing.T) { 44 | fixt := []Fixture{ 45 | {"Это нужно сделать завтра после обеда", 33, "завтра после обеда", (15 + 24) * time.Hour}, 46 | {"Это нужно сделать завтра утром", 33, "завтра утром", (8 + 24) * time.Hour}, 47 | {"Это нужно было сделать вчера утром", 42, "вчера утром", (8 - 24) * time.Hour}, 48 | {"Это нужно было сделать вчера после обеда", 42, "вчера после обеда", (15 - 24) * time.Hour}, 49 | {"помыть окна до вечера", 22, "до вечера", 18 * time.Hour}, 50 | {"помыть окна до обеда", 22, "до обеда", 12 * time.Hour}, 51 | {"сделать это к вечеру", 22, "к вечеру", 18 * time.Hour}, 52 | {"помыть окна завтра утром", 22, "завтра утром", 32 * time.Hour}, 53 | {"написать письмо во вторник после обеда", 50, "после обеда", 15 * time.Hour}, 54 | {"написать письмо до утра ", 30, "до утра", 8 * time.Hour}, 55 | {"к вечеру", 0, "к вечеру", 18 * time.Hour}, 56 | } 57 | 58 | w := when.New(nil) 59 | w.Add( 60 | ru.CasualDate(rules.Skip), 61 | ru.CasualTime(rules.Override), 62 | ) 63 | 64 | ApplyFixtures(t, "ru.CasualDate|ru.CasualTime", w, fixt) 65 | } 66 | -------------------------------------------------------------------------------- /rules/ru/casual_time.go: -------------------------------------------------------------------------------- 1 | package ru 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/AlekSi/pointer" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | // https://play.golang.org/p/IUbYhm7Nu- 13 | 14 | func CasualTime(s rules.Strategy) rules.Rule { 15 | return &rules.F{ 16 | RegExp: regexp.MustCompile(`(?i)(?:\P{L}|^)((это|этим|этот|этим|до|к|после)?\s*(утр(?:ом|а|у)|вечер(?:у|ом|а)|обеда?))(?:\P{L}|$)`), 17 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 18 | 19 | lower := strings.ToLower(strings.TrimSpace(m.String())) 20 | 21 | if (c.Hour != nil || c.Minute != nil) && s == rules.Override { 22 | return false, nil 23 | } 24 | 25 | switch { 26 | case strings.Contains(lower, "после обеда"): 27 | if o.Afternoon != 0 { 28 | c.Hour = &o.Afternoon 29 | } else { 30 | c.Hour = pointer.ToInt(15) 31 | } 32 | c.Minute = pointer.ToInt(0) 33 | case strings.Contains(lower, "вечер"): 34 | if o.Evening != 0 { 35 | c.Hour = &o.Evening 36 | } else { 37 | c.Hour = pointer.ToInt(18) 38 | } 39 | c.Minute = pointer.ToInt(0) 40 | case strings.Contains(lower, "утр"): 41 | if o.Morning != 0 { 42 | c.Hour = &o.Morning 43 | } else { 44 | c.Hour = pointer.ToInt(8) 45 | } 46 | c.Minute = pointer.ToInt(0) 47 | case strings.Contains(lower, "обед"): 48 | if o.Noon != 0 { 49 | c.Hour = &o.Noon 50 | } else { 51 | c.Hour = pointer.ToInt(12) 52 | } 53 | c.Minute = pointer.ToInt(0) 54 | } 55 | 56 | return true, nil 57 | }, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rules/ru/date.go: -------------------------------------------------------------------------------- 1 | package ru 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/olebedev/when/rules" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // https://go.dev/play/p/YsVdaraCwIP 14 | 15 | func Date(s rules.Strategy) rules.Rule { 16 | return &rules.F{ 17 | RegExp: regexp.MustCompile(`(?i)(?:\b|^)(\d{1,2})\s*(` + MONTHS_PATTERN + `)(?:\s*(\d{4}))?(?:\s*в\s*(\d{1,2}):(\d{2}))?(?:\b|$)`), 18 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 19 | if (c.Day != nil || c.Month != nil || c.Year != nil) || s != rules.Override { 20 | return false, nil 21 | } 22 | 23 | day, err := strconv.Atoi(m.Captures[0]) 24 | if err != nil { 25 | return false, errors.Wrap(err, "date rule: day") 26 | } 27 | 28 | month, ok := MONTHS[strings.ToLower(m.Captures[1])] 29 | if !ok { 30 | return false, errors.New("date rule: invalid month") 31 | } 32 | 33 | year := time.Now().Year() 34 | if m.Captures[2] != "" { 35 | year, err = strconv.Atoi(m.Captures[2]) 36 | if err != nil { 37 | return false, errors.Wrap(err, "date rule: year") 38 | } 39 | } 40 | 41 | hour, minute := 0, 0 42 | if m.Captures[3] != "" && m.Captures[4] != "" { 43 | hour, err = strconv.Atoi(m.Captures[3]) 44 | if err != nil { 45 | return false, errors.Wrap(err, "date rule: hour") 46 | } 47 | minute, err = strconv.Atoi(m.Captures[4]) 48 | if err != nil { 49 | return false, errors.Wrap(err, "date rule: minute") 50 | } 51 | } 52 | 53 | c.Day = &day 54 | c.Month = pointerToInt(int(month)) 55 | c.Year = &year 56 | c.Hour = &hour 57 | c.Minute = &minute 58 | 59 | return true, nil 60 | }, 61 | } 62 | } 63 | 64 | func pointerToInt(v int) *int { 65 | return &v 66 | } 67 | -------------------------------------------------------------------------------- /rules/ru/date_test.go: -------------------------------------------------------------------------------- 1 | package ru_test 2 | 3 | import ( 4 | "github.com/olebedev/when" 5 | "github.com/olebedev/when/rules" 6 | "github.com/olebedev/when/rules/ru" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestDate(t *testing.T) { 12 | w := when.New(nil) 13 | w.Add(ru.Date(rules.Override)) 14 | 15 | fixt := []Fixture{ 16 | // Simple dates 17 | {"встреча 15 января 2024", 15, "15 января 2024", time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC).Sub(null)}, 18 | {"5 марта 2025 запланирована встреча", 0, "5 марта 2025", time.Date(2025, 3, 5, 0, 0, 0, 0, time.UTC).Sub(null)}, 19 | {"31 декабря 2023", 0, "31 декабря 2023", time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC).Sub(null)}, 20 | 21 | // Dates with time 22 | {"15 января 2024 в 9:30", 0, "15 января 2024 в 9:30", time.Date(2024, 1, 15, 9, 30, 0, 0, time.UTC).Sub(null)}, 23 | {"5 марта 2025 в 15:00 запланирована встреча", 0, "5 марта 2025 в 15:00", time.Date(2025, 3, 5, 15, 0, 0, 0, time.UTC).Sub(null)}, 24 | {"31 декабря 2023 в 23:59", 0, "31 декабря 2023 в 23:59", time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC).Sub(null)}, 25 | } 26 | 27 | ApplyFixtures(t, "ru.Date", w, fixt) 28 | } 29 | 30 | func TestDateNil(t *testing.T) { 31 | w := when.New(nil) 32 | w.Add(ru.Date(rules.Override)) 33 | 34 | fixt := []Fixture{ 35 | {"это текст без даты", 0, "", 0}, 36 | {"15", 0, "", 0}, 37 | {"15 чего-то", 0, "", 0}, 38 | } 39 | 40 | ApplyFixturesNil(t, "ru.Date nil", w, fixt) 41 | } 42 | -------------------------------------------------------------------------------- /rules/ru/deadline.go: -------------------------------------------------------------------------------- 1 | package ru 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/AlekSi/pointer" 10 | "github.com/olebedev/when/rules" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // https://play.golang.org/p/A-cF_q9U34 15 | 16 | func Deadline(s rules.Strategy) rules.Rule { 17 | return &rules.F{ 18 | RegExp: regexp.MustCompile("(?i)(?:\\P{L}|^)" + 19 | "(в\\sтечении|за|через)\\s*" + 20 | "(" + INTEGER_WORDS_PATTERN + "|[0-9]+|полу?|несколько|нескольких)?\\s*" + 21 | "(секунд(?:у|ы)?|минут(?:у|ы)?|час(?:а|ов)?|день|дня|дней|недел(?:я|ь|и|ю)|месяц(?:а|ев)?|год(?:а)?|лет)\\s*" + 22 | "(?:\\P{L}|$)"), 23 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 24 | if c.Duration != 0 && s != rules.Override { 25 | return false, nil 26 | } 27 | 28 | numStr := strings.TrimSpace(m.Captures[1]) 29 | 30 | var num int 31 | var err error 32 | 33 | if n, ok := INTEGER_WORDS[numStr]; ok { 34 | num = n 35 | } else if numStr == "" { 36 | num = 1 37 | } else if strings.Contains(numStr, "неск") { 38 | num = 3 39 | } else if strings.Contains(numStr, "пол") { 40 | // pass 41 | } else { 42 | num, err = strconv.Atoi(numStr) 43 | if err != nil { 44 | return false, errors.Wrapf(err, "convert '%s' to int", numStr) 45 | } 46 | } 47 | 48 | exponent := strings.TrimSpace(m.Captures[2]) 49 | 50 | if !strings.Contains(numStr, "пол") { 51 | switch { 52 | case strings.Contains(exponent, "секунд"): 53 | c.Duration = time.Duration(num) * time.Second 54 | case strings.Contains(exponent, "мин"): 55 | c.Duration = time.Duration(num) * time.Minute 56 | case strings.Contains(exponent, "час"): 57 | c.Duration = time.Duration(num) * time.Hour 58 | case strings.Contains(exponent, "дн") || strings.Contains(exponent, "день"): 59 | c.Duration = time.Duration(num) * 24 * time.Hour 60 | case strings.Contains(exponent, "недел"): 61 | c.Duration = time.Duration(num) * 7 * 24 * time.Hour 62 | case strings.Contains(exponent, "месяц"): 63 | c.Month = pointer.ToInt((int(ref.Month()) + num) % 12) 64 | case strings.Contains(exponent, "год") || strings.Contains(exponent, "лет"): 65 | c.Year = pointer.ToInt(ref.Year() + num) 66 | } 67 | } else { 68 | switch { 69 | case strings.Contains(exponent, "час"): 70 | c.Duration = 30 * time.Minute 71 | case strings.Contains(exponent, "дн") || strings.Contains(exponent, "день"): 72 | c.Duration = 12 * time.Hour 73 | case strings.Contains(exponent, "недел"): 74 | c.Duration = 7 * 12 * time.Hour 75 | case strings.Contains(exponent, "месяц"): 76 | // 2 weeks 77 | c.Duration = 14 * 24 * time.Hour 78 | case strings.Contains(exponent, "год") || strings.Contains(exponent, "лет"): 79 | c.Month = pointer.ToInt((int(ref.Month()) + 6) % 12) 80 | } 81 | } 82 | 83 | return true, nil 84 | }, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /rules/ru/deadline_test.go: -------------------------------------------------------------------------------- 1 | package ru_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/ru" 10 | ) 11 | 12 | func TestDeadline(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"нужно сделать это в течении получаса", 33, "в течении получаса", time.Hour / 2}, 15 | {"нужно сделать это в течении одного часа", 33, "в течении одного часа", time.Hour}, 16 | {"нужно сделать это за один час", 33, "за один час", time.Hour}, 17 | {"за 5 минут", 0, "за 5 минут", time.Minute * 5}, 18 | {"Через 5 минут я пойду домой.", 0, "Через 5 минут", time.Minute * 5}, 19 | {"Нам необходимо сделать это за 10 дней.", 50, "за 10 дней", 10 * 24 * time.Hour}, 20 | {"Нам необходимо сделать это за пять дней.", 50, "за пять дней", 5 * 24 * time.Hour}, 21 | {"Нам необходимо сделать это через 5 дней.", 50, "через 5 дней", 5 * 24 * time.Hour}, 22 | {"Через 5 секунд нужно убрать машину", 0, "Через 5 секунд", 5 * time.Second}, 23 | {"за две недели", 0, "за две недели", 14 * 24 * time.Hour}, 24 | {"через месяц", 0, "через месяц", 31 * 24 * time.Hour}, 25 | {"за месяц", 0, "за месяц", 31 * 24 * time.Hour}, 26 | {"за несколько месяцев", 0, "за несколько месяцев", 91 * 24 * time.Hour}, 27 | {"за один год", 0, "за один год", 366 * 24 * time.Hour}, 28 | {"за неделю", 0, "за неделю", 7 * 24 * time.Hour}, 29 | } 30 | 31 | w := when.New(nil) 32 | w.Add(ru.Deadline(rules.Skip)) 33 | 34 | ApplyFixtures(t, "ru.Deadline", w, fixt) 35 | } 36 | -------------------------------------------------------------------------------- /rules/ru/dot_date_time.go: -------------------------------------------------------------------------------- 1 | package ru 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // https://go.dev/play/p/vRzLhHHupUJ 13 | 14 | func DotDateTime(s rules.Strategy) rules.Rule { 15 | return &rules.F{ 16 | RegExp: regexp.MustCompile(`(?i)(?:^|\b)(\d{2})\.(\d{2})\.(\d{4})(?:\s+(\d{2}):(\d{2}))?(?:\b|$)`), 17 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 18 | if (c.Day != nil || c.Month != nil || c.Year != nil || c.Hour != nil || c.Minute != nil) && s != rules.Override { 19 | return false, nil 20 | } 21 | 22 | day, err := strconv.Atoi(m.Captures[0]) 23 | if err != nil { 24 | return false, errors.Wrap(err, "dot date time rule: day") 25 | } 26 | 27 | month, err := strconv.Atoi(m.Captures[1]) 28 | if err != nil { 29 | return false, errors.Wrap(err, "dot date time rule: month") 30 | } 31 | 32 | year, err := strconv.Atoi(m.Captures[2]) 33 | if err != nil { 34 | return false, errors.Wrap(err, "dot date time rule: year") 35 | } 36 | 37 | hour, minute := 0, 0 38 | if m.Captures[3] != "" && m.Captures[4] != "" { 39 | hour, err = strconv.Atoi(m.Captures[3]) 40 | if err != nil { 41 | return false, errors.Wrap(err, "dot date time rule: hour") 42 | } 43 | minute, err = strconv.Atoi(m.Captures[4]) 44 | if err != nil { 45 | return false, errors.Wrap(err, "dot date time rule: minute") 46 | } 47 | } 48 | 49 | if day > 0 && day <= 31 && month > 0 && month <= 12 { 50 | c.Day = &day 51 | c.Month = &month 52 | c.Year = &year 53 | c.Hour = &hour 54 | c.Minute = &minute 55 | return true, nil 56 | } 57 | 58 | return false, nil 59 | }, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rules/ru/dot_date_time_test.go: -------------------------------------------------------------------------------- 1 | package ru_test 2 | 3 | import ( 4 | "github.com/olebedev/when" 5 | "github.com/olebedev/when/rules" 6 | "github.com/olebedev/when/rules/ru" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestDotDateTime(t *testing.T) { 12 | w := when.New(nil) 13 | w.Add(ru.DotDateTime(rules.Override)) 14 | 15 | fixt := []Fixture{ 16 | // Basic date/time formats 17 | {"встреча 15.01.2024 09:30", 15, "15.01.2024 09:30", time.Date(2024, 1, 15, 9, 30, 0, 0, time.UTC).Sub(null)}, 18 | {"05.03.2025 15:00 запланирована встреча", 0, "05.03.2025 15:00", time.Date(2025, 3, 5, 15, 0, 0, 0, time.UTC).Sub(null)}, 19 | {"31.12.2023 23:59", 0, "31.12.2023 23:59", time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC).Sub(null)}, 20 | } 21 | 22 | ApplyFixtures(t, "ru.DateTime", w, fixt) 23 | } 24 | 25 | func TestDotDateTimeNil(t *testing.T) { 26 | w := when.New(nil) 27 | w.Add(ru.DotDateTime(rules.Override)) 28 | 29 | fixt := []Fixture{ 30 | {"это текст без даты и времени", 0, "", 0}, 31 | {"15.01", 0, "", 0}, 32 | {"32.01.2024 15:00", 0, "", 0}, // некорректный день 33 | {"15.13.2024 15:00", 0, "", 0}, // некорректный месяц 34 | } 35 | 36 | ApplyFixturesNil(t, "ru.DateTime nil", w, fixt) 37 | } 38 | -------------------------------------------------------------------------------- /rules/ru/hour.go: -------------------------------------------------------------------------------- 1 | package ru 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | /* 13 | "5pm" 14 | "5 pm" 15 | "5am" 16 | "5pm" 17 | "5A." 18 | "5P." 19 | "11 P.M." 20 | https://play.golang.org/p/w2PeQ3l_rp 21 | */ 22 | 23 | func Hour(s rules.Strategy) rules.Rule { 24 | return &rules.F{ 25 | RegExp: regexp.MustCompile("(?i)(?:\\W|^)" + 26 | "(" + INTEGER_WORDS_PATTERN + "|\\d{1,2})" + 27 | "(?:\\s*час(?:а|ов|ам)?)?(?:\\s*(утра|вечера|дня))" + 28 | "(?:\\P{L}|$)"), 29 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 30 | if c.Hour != nil && s != rules.Override { 31 | return false, nil 32 | } 33 | 34 | var hour int 35 | var err error 36 | 37 | if n, ok := INTEGER_WORDS[m.Captures[0]]; ok { 38 | hour = n 39 | } else { 40 | hour, err = strconv.Atoi(m.Captures[0]) 41 | if err != nil { 42 | return false, errors.Wrap(err, "hour rule") 43 | } 44 | } 45 | 46 | if hour > 12 { 47 | return false, nil 48 | } 49 | zero := 0 50 | 51 | switch m.Captures[1] { 52 | case "утра": 53 | c.Hour = &hour 54 | case "вечера", "дня": 55 | if hour < 12 { 56 | hour += 12 57 | } 58 | c.Hour = &hour 59 | } 60 | c.Minute = &zero 61 | 62 | return true, nil 63 | }, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /rules/ru/hour_minute.go: -------------------------------------------------------------------------------- 1 | package ru 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | /* 13 | {"5:30pm", 0, "5:30pm", 0}, 14 | {"5:30 pm", 0, "5:30 pm", 0}, 15 | {"7-10pm", 0, "7-10pm", 0}, 16 | {"5-30", 0, "5-30", 0}, 17 | {"05:30pm", 0, "05:30pm", 0}, 18 | {"05:30 pm", 0, "05:30 pm", 0}, 19 | {"05:30", 0, "05:30", 0}, 20 | {"05-30", 0, "05-30", 0}, 21 | {"7-10 pm", 0, "7-10 pm", 0}, 22 | {"11.1pm", 0, "11.1pm", 0}, 23 | {"11.10 pm", 0, "11.10 pm", 0}, 24 | 25 | https://go.dev/play/p/QiSvUkrni6N 26 | */ 27 | 28 | // 1. - int 29 | // 2. - int 30 | // 3. - ext? 31 | 32 | func HourMinute(s rules.Strategy) rules.Rule { 33 | return &rules.F{ 34 | RegExp: regexp.MustCompile("(?i)(?:\\A|\\s|\\D)" + 35 | "((?:[0-1]{0,1}[0-9])|(?:2[0-3]))" + 36 | "(?:\\:|:|\\-|\\.)" + 37 | "((?:[0-5][0-9]))" + 38 | "(?:\\s*(утра|вечера|дня))?" + 39 | "(?:\\s|\\D|\\z)"), 40 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 41 | if (c.Hour != nil || c.Minute != nil) && s != rules.Override { 42 | return false, nil 43 | } 44 | 45 | hour, err := strconv.Atoi(m.Captures[0]) 46 | if err != nil { 47 | return false, errors.Wrap(err, "hour minute rule") 48 | } 49 | 50 | minutes, err := strconv.Atoi(m.Captures[1]) 51 | if err != nil { 52 | return false, errors.Wrap(err, "hour minute rule") 53 | } 54 | 55 | c.Minute = &minutes 56 | 57 | if m.Captures[2] != "" { 58 | if hour > 12 { 59 | return false, nil 60 | } 61 | switch m.Captures[2] { 62 | case "утра": // am 63 | c.Hour = &hour 64 | case "вечера", "дня": // pm 65 | if hour < 12 { 66 | hour += 12 67 | } 68 | c.Hour = &hour 69 | } 70 | } else { 71 | c.Hour = &hour 72 | } 73 | 74 | return true, nil 75 | }, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /rules/ru/hour_minute_test.go: -------------------------------------------------------------------------------- 1 | package ru_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/ru" 10 | ) 11 | 12 | func TestHourMinute(t *testing.T) { 13 | w := when.New(nil) 14 | w.Add(ru.HourMinute(rules.Override)) 15 | 16 | fixtok := []Fixture{ 17 | {"5:30вечера", 0, "5:30вечера", (17 * time.Hour) + (30 * time.Minute)}, 18 | {"в 5:30 вечера", 3, "5:30 вечера", (17 * time.Hour) + (30 * time.Minute)}, 19 | {"в 5:59 вечера", 3, "5:59 вечера", (17 * time.Hour) + (59 * time.Minute)}, 20 | {"в 5-59 вечера", 3, "5-59 вечера", (17 * time.Hour) + (59 * time.Minute)}, 21 | {"в 17-59 вечерело", 3, "17-59", (17 * time.Hour) + (59 * time.Minute)}, 22 | {"до 11.10 вечера", 5, "11.10 вечера", (23 * time.Hour) + (10 * time.Minute)}, 23 | } 24 | 25 | fixtnil := []Fixture{ 26 | {"28:30вечера", 0, "", 0}, 27 | {"12:61вечера", 0, "", 0}, 28 | {"24:10", 0, "", 0}, 29 | } 30 | 31 | // ApplyFixtures(t, "ru.HourMinute", w, fixtok) 32 | 33 | // ApplyFixturesNil(t, "on.HourMinute nil", w, fixtnil) 34 | 35 | w.Add(ru.Hour(rules.Skip)) 36 | // ApplyFixtures(t, "ru.HourMinute|ru.Hour", w, fixtok) 37 | ApplyFixturesNil(t, "ru.HourMinute|ru.Hour nil", w, fixtnil) 38 | 39 | w = when.New(nil) 40 | w.Add( 41 | ru.Hour(rules.Override), 42 | ru.HourMinute(rules.Override), 43 | ) 44 | 45 | ApplyFixtures(t, "ru.Hour|ru.HourMinute", w, fixtok) 46 | ApplyFixturesNil(t, "ru.Hour|ru.HourMinute nil", w, fixtnil) 47 | } 48 | -------------------------------------------------------------------------------- /rules/ru/hour_test.go: -------------------------------------------------------------------------------- 1 | package ru_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/ru" 10 | ) 11 | 12 | func TestHour(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"5вечера", 0, "5вечера", 17 * time.Hour}, 15 | {"в 5 вечера", 3, "5 вечера", 17 * time.Hour}, 16 | {"нужно к 5 часам вечера", 14, "5 часам вечера", 17 * time.Hour}, 17 | {"в три часа дня", 3, "три часа дня", 15 * time.Hour}, 18 | {"в час дня", 3, "час дня", 13 * time.Hour}, 19 | {"в одиннадцать часов утра", 3, "одиннадцать часов утра", 11 * time.Hour}, 20 | {"в семь вечера", 3, "семь вечера", 19 * time.Hour}, 21 | } 22 | 23 | w := when.New(nil) 24 | w.Add(ru.Hour(rules.Override)) 25 | 26 | ApplyFixtures(t, "en.Hour", w, fixt) 27 | } 28 | -------------------------------------------------------------------------------- /rules/ru/ru.go: -------------------------------------------------------------------------------- 1 | package ru 2 | 3 | import ( 4 | "github.com/olebedev/when/rules" 5 | "time" 6 | ) 7 | 8 | var All = []rules.Rule{ 9 | Weekday(rules.Override), 10 | CasualDate(rules.Override), 11 | CasualTime(rules.Override), 12 | Hour(rules.Override), 13 | HourMinute(rules.Override), 14 | Deadline(rules.Override), 15 | Date(rules.Override), 16 | DotDateTime(rules.Override), 17 | } 18 | 19 | var WEEKDAY_OFFSET = map[string]int{ 20 | "воскресенье": 0, 21 | "воскресенья": 0, 22 | "воск": 0, 23 | "понедельник": 1, 24 | "понедельнику": 1, 25 | "понедельника": 1, 26 | "пн": 1, 27 | "вторник": 2, 28 | "вторника": 2, 29 | "вторнику": 2, 30 | "вт": 2, 31 | "среда": 3, 32 | "среду": 3, 33 | "среде": 3, 34 | "ср": 3, 35 | "четверг": 4, 36 | "четверга": 4, 37 | "четвергу": 4, 38 | "чт": 4, 39 | "пятница": 5, 40 | "пятнице": 5, 41 | "пятницы": 5, 42 | "пятницу": 5, 43 | "пт": 5, 44 | "суббота": 6, 45 | "субботы": 6, 46 | "субботе": 6, 47 | "субботу": 6, 48 | "сб": 6, 49 | } 50 | 51 | var WEEKDAY_OFFSET_PATTERN = "(?:воскресенье|воскресенья|воск|понедельник|понедельнику|понедельника|пн|вторник|вторника|вторнику|вт|среда|среду|среде|ср|четверг|четверга|четвергу|чт|пятница|пятнице|пятницы|пятницу|пт|суббота|субботы|субботе|субботу|сб)" 52 | 53 | var INTEGER_WORDS = map[string]int{ 54 | "час": 1, 55 | "один": 1, 56 | "одну": 1, 57 | "одного": 1, 58 | "два": 2, 59 | "две": 2, 60 | "три": 3, 61 | "четыре": 4, 62 | "пять": 5, 63 | "шесть": 6, 64 | "семь": 7, 65 | "восемь": 8, 66 | "девять": 9, 67 | "десять": 10, 68 | "одиннадцать": 11, 69 | "двенадцать": 12, 70 | } 71 | 72 | var INTEGER_WORDS_PATTERN = `(?:час|один|одну|одного|два|две|три|четыре|пять|шесть|семь|восемь|девять|десять|одиннадцать|двенадцать)` 73 | 74 | var MONTHS = map[string]time.Month{ 75 | "января": time.January, 76 | "февраля": time.February, 77 | "марта": time.March, 78 | "апреля": time.April, 79 | "мая": time.May, 80 | "июня": time.June, 81 | "июля": time.July, 82 | "августа": time.August, 83 | "сентября": time.September, 84 | "октября": time.October, 85 | "ноября": time.November, 86 | "декабря": time.December, 87 | } 88 | 89 | var MONTHS_PATTERN = `(?:января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)` 90 | -------------------------------------------------------------------------------- /rules/ru/ru_test.go: -------------------------------------------------------------------------------- 1 | package ru_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules/ru" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var null = time.Date(2016, time.January, 6, 0, 0, 0, 0, time.UTC) 13 | 14 | type Fixture struct { 15 | Text string 16 | Index int 17 | Phrase string 18 | Diff time.Duration 19 | } 20 | 21 | func ApplyFixtures(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 22 | for i, f := range fixt { 23 | res, err := w.Parse(f.Text, null) 24 | require.Nil(t, err, "[%s] err #%d - %s", name, i, f.Text) 25 | require.NotNil(t, res, "[%s] res #%d - %s", name, i, f.Text) 26 | require.Equal(t, f.Index, res.Index, "[%s] index #%d - %s", name, i, f.Text) 27 | require.Equal(t, f.Phrase, res.Text, "[%s] text #%d - %s", name, i, f.Text) 28 | require.Equal(t, f.Diff, res.Time.Sub(null), "[%s] diff #%d - %s", name, i, f.Text) 29 | } 30 | } 31 | 32 | func ApplyFixturesNil(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 33 | for i, f := range fixt { 34 | res, err := w.Parse(f.Text, null) 35 | require.Nil(t, err, "[%s] err #%d", name, i) 36 | require.Nil(t, res, "[%s] res #%d", name, i) 37 | } 38 | } 39 | 40 | func ApplyFixturesErr(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 41 | for i, f := range fixt { 42 | _, err := w.Parse(f.Text, null) 43 | require.NotNil(t, err, "[%s] err #%d", name, i) 44 | require.Equal(t, f.Phrase, err.Error(), "[%s] err text #%d", name, i) 45 | } 46 | } 47 | 48 | func TestAll(t *testing.T) { 49 | w := when.New(nil) 50 | w.Add(ru.All...) 51 | 52 | // complex cases 53 | fixt := []Fixture{ 54 | {"завтра в 11:10 вечера", 0, "завтра в 11:10 вечера", (47 * time.Hour) + (10 * time.Minute)}, 55 | {"вечером в следующий понедельник", 0, "вечером в следующий понедельник", ((5 * 24) + 18) * time.Hour}, 56 | {"вечером в прошлый понедельник", 0, "вечером в прошлый понедельник", ((-2 * 24) + 18) * time.Hour}, 57 | {"в следующий понедельник вечером", 3, "следующий понедельник вечером", ((5 * 24) + 18) * time.Hour}, 58 | {"в Пятницу после обеда", 0, "в Пятницу после обеда", ((2 * 24) + 15) * time.Hour}, 59 | {"в следующий вторник в 14:00", 3, "следующий вторник в 14:00", ((6 * 24) + 14) * time.Hour}, 60 | {"в следующий вторник в четыре вечера", 3, "следующий вторник в четыре вечера", ((6 * 24) + 16) * time.Hour}, 61 | {"в следующую среду в 2:25 вечера", 3, "следующую среду в 2:25 вечера", (((7 * 24) + 14) * time.Hour) + (25 * time.Minute)}, 62 | {"в 11 утра в прошлый вторник", 3, "11 утра в прошлый вторник", -13 * time.Hour}, 63 | 64 | {"написать письмо во вторник после обеда", 30, "во вторник после обеда", ((6 * 24) + 15) * time.Hour}, 65 | {"написать письмо ко вторнику", 30, "ко вторнику", 6 * 24 * time.Hour}, 66 | {"написать письмо до утра субботы ", 30, "до утра субботы", ((3 * 24) + 8) * time.Hour}, 67 | {"написать письмо к субботе после обеда ", 30, "к субботе после обеда", ((3 * 24) + 15) * time.Hour}, 68 | {"В субботу вечером", 0, "В субботу вечером", ((3 * 24) + 18) * time.Hour}, 69 | 70 | {"встреча 15 января 2024", 15, "15 января 2024", time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC).Sub(null)}, 71 | {"5 марта 2025 запланирована встреча", 0, "5 марта 2025", time.Date(2025, 3, 5, 0, 0, 0, 0, time.UTC).Sub(null)}, 72 | {"31 декабря 2023", 0, "31 декабря 2023", time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC).Sub(null)}, 73 | {"15 января 2024 в 9:30", 0, "15 января 2024 в 9:30", time.Date(2024, 1, 15, 9, 30, 0, 0, time.UTC).Sub(null)}, 74 | {"5 марта 2025 в 15:00 запланирована встреча", 0, "5 марта 2025 в 15:00", time.Date(2025, 3, 5, 15, 0, 0, 0, time.UTC).Sub(null)}, 75 | {"31 декабря 2023 в 23:59", 0, "31 декабря 2023 в 23:59", time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC).Sub(null)}, 76 | {"31 декабря", 0, "31 декабря", time.Date(time.Now().Year(), 12, 31, 0, 0, 0, 0, time.UTC).Sub(null)}, 77 | {"встреча 15.01.2024 09:30", 15, "15.01.2024 09:30", time.Date(2024, 1, 15, 9, 30, 0, 0, time.UTC).Sub(null)}, 78 | {"05.03.2025 15:00 запланирована встреча", 0, "05.03.2025 15:00", time.Date(2025, 3, 5, 15, 0, 0, 0, time.UTC).Sub(null)}, 79 | {"31.12.2023 23:59", 0, "31.12.2023 23:59", time.Date(2023, 12, 31, 23, 59, 0, 0, time.UTC).Sub(null)}, 80 | {"31.12.2023", 0, "31.12.2023", time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC).Sub(null)}, 81 | } 82 | 83 | ApplyFixtures(t, "ru.All...", w, fixt) 84 | } 85 | -------------------------------------------------------------------------------- /rules/ru/weekday.go: -------------------------------------------------------------------------------- 1 | package ru 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | ) 10 | 11 | // https://play.golang.org/p/aRWlil_64M 12 | 13 | func Weekday(s rules.Strategy) rules.Rule { 14 | return &rules.F{ 15 | RegExp: regexp.MustCompile("(?i)(?:\\P{L}|^)" + 16 | "(?:(на|во?|ко?|до|эт(?:от|ой|у|а)?|прошл(?:ую|ый|ая)|последн(?:юю|ий|ее|ая)|следующ(?:ую|ее|ая|ий))\\s*)?" + 17 | "(" + WEEKDAY_OFFSET_PATTERN[3:] + // skip '(?:' 18 | "(?:\\s*на\\s*(этой|прошлой|следующей)\\s*неделе)?" + 19 | "(?:\\P{L}|$)"), 20 | 21 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 22 | 23 | day := strings.ToLower(strings.TrimSpace(m.Captures[1])) 24 | norm := m.Captures[2] 25 | if norm == "" { 26 | norm = m.Captures[0] 27 | } 28 | if norm == "" { 29 | norm = "следующ" 30 | } 31 | norm = strings.ToLower(strings.TrimSpace(norm)) 32 | 33 | dayInt, ok := WEEKDAY_OFFSET[day] 34 | if !ok { 35 | return false, nil 36 | } 37 | 38 | if c.Duration != 0 && s != rules.Override { 39 | return false, nil 40 | } 41 | 42 | // Switch: 43 | switch { 44 | case strings.Contains(norm, "прошл") || strings.Contains(norm, "последн"): 45 | diff := int(ref.Weekday()) - dayInt 46 | if diff > 0 { 47 | c.Duration = -time.Duration(diff*24) * time.Hour 48 | } else if diff < 0 { 49 | c.Duration = -time.Duration(7+diff) * 24 * time.Hour 50 | } else { 51 | c.Duration = -(7 * 24 * time.Hour) 52 | } 53 | case strings.Contains(norm, "следующ"), 54 | norm == "в", 55 | norm == "к", 56 | strings.Contains(norm, "во"), 57 | strings.Contains(norm, "ко"), 58 | strings.Contains(norm, "до"): 59 | diff := dayInt - int(ref.Weekday()) 60 | if diff > 0 { 61 | c.Duration = time.Duration(diff*24) * time.Hour 62 | } else if diff < 0 { 63 | c.Duration = time.Duration(7+diff) * 24 * time.Hour 64 | } else { 65 | c.Duration = 7 * 24 * time.Hour 66 | } 67 | case strings.Contains(norm, "эт"): 68 | if int(ref.Weekday()) < dayInt { 69 | diff := dayInt - int(ref.Weekday()) 70 | if diff > 0 { 71 | c.Duration = time.Duration(diff*24) * time.Hour 72 | } else if diff < 0 { 73 | c.Duration = time.Duration(7+diff) * 24 * time.Hour 74 | } else { 75 | c.Duration = 7 * 24 * time.Hour 76 | } 77 | } else if int(ref.Weekday()) > dayInt { 78 | diff := int(ref.Weekday()) - dayInt 79 | if diff > 0 { 80 | c.Duration = -time.Duration(diff*24) * time.Hour 81 | } else if diff < 0 { 82 | c.Duration = -time.Duration(7+diff) * 24 * time.Hour 83 | } else { 84 | c.Duration = -(7 * 24 * time.Hour) 85 | } 86 | } 87 | } 88 | 89 | return true, nil 90 | }, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /rules/ru/weekday_test.go: -------------------------------------------------------------------------------- 1 | package ru_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/ru" 10 | ) 11 | 12 | func TestWeekday(t *testing.T) { 13 | // current is Friday 14 | fixt := []Fixture{ 15 | // past/last 16 | {"это нужно было сделать в прошлый Понедельник", 45, "прошлый Понедельник", -(2 * 24 * time.Hour)}, 17 | {"прошлая суббота", 0, "прошлая суббота", -(4 * 24 * time.Hour)}, 18 | {"прошлая пятница", 0, "прошлая пятница", -(5 * 24 * time.Hour)}, 19 | {"в последнюю среду", 3, "последнюю среду", -(7 * 24 * time.Hour)}, 20 | {"в прошлый вторник", 3, "прошлый вторник", -(24 * time.Hour)}, 21 | 22 | // next 23 | {"в следующий вторник", 3, "следующий вторник", 6 * 24 * time.Hour}, 24 | {"напиши мне в следующую среду, договоримся", 23, "следующую среду", 7 * 24 * time.Hour}, 25 | {"следующая суббота", 0, "следующая суббота", 3 * 24 * time.Hour}, 26 | {"в следующую суббота", 3, "следующую суббота", 3 * 24 * time.Hour}, 27 | 28 | // this 29 | {"в этот вторник", 3, "этот вторник", -(24 * time.Hour)}, 30 | {"напиши мне в эту среду, договоримся", 23, "эту среду", 0}, 31 | {"эта суббота", 0, "эта суббота", 3 * 24 * time.Hour}, 32 | {"во вторник", 0, "во вторник", 6 * 24 * time.Hour}, 33 | {"в субботу", 0, "в субботу", 3 * 24 * time.Hour}, 34 | } 35 | 36 | w := when.New(nil) 37 | 38 | w.Add(ru.Weekday(rules.Override)) 39 | 40 | ApplyFixtures(t, "ru.Weekday", w, fixt) 41 | } 42 | 43 | func TestWeekdayNil(t *testing.T) { 44 | fixt := []Fixture{ 45 | {"завтра", 0, "", 0}, 46 | } 47 | 48 | w := when.New(nil) 49 | 50 | w.Add(ru.Weekday(rules.Override)) 51 | 52 | ApplyFixturesNil(t, "ru.Weekday nil", w, fixt) 53 | } 54 | -------------------------------------------------------------------------------- /rules/rules.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "regexp" 5 | "time" 6 | ) 7 | 8 | type Strategy int 9 | 10 | const ( 11 | Skip Strategy = iota 12 | Merge 13 | Override 14 | ) 15 | 16 | type Rule interface { 17 | Find(string) *Match 18 | } 19 | 20 | type Options struct { 21 | Afternoon, Evening, Morning, Noon int 22 | 23 | Distance int 24 | 25 | MatchByOrder bool 26 | 27 | // TODO 28 | // WeekStartsOn time.Weekday 29 | } 30 | 31 | type Match struct { 32 | Left, Right int 33 | Text string 34 | Captures []string 35 | Order float64 36 | Applier func(*Match, *Context, *Options, time.Time) (bool, error) 37 | } 38 | 39 | func (m Match) String() string { return m.Text } 40 | 41 | func (m *Match) Apply(c *Context, o *Options, t time.Time) (bool, error) { 42 | return m.Applier(m, c, o, t) 43 | } 44 | 45 | type F struct { 46 | RegExp *regexp.Regexp 47 | Applier func(*Match, *Context, *Options, time.Time) (bool, error) 48 | } 49 | 50 | func (f *F) Find(text string) *Match { 51 | m := &Match{ 52 | Applier: f.Applier, 53 | Left: -1, 54 | } 55 | 56 | indexes := f.RegExp.FindStringSubmatchIndex(text) 57 | 58 | length := len(indexes) 59 | if length <= 2 { 60 | 61 | return nil 62 | } 63 | 64 | for i := 2; i < length; i += 2 { 65 | if m.Left == -1 && indexes[i] >= 0 { 66 | m.Left = indexes[i] 67 | } 68 | // check if capture was found 69 | if indexes[i] >= 0 && indexes[i+1] >= 0 { 70 | m.Captures = append(m.Captures, text[indexes[i]:indexes[i+1]]) 71 | m.Right = indexes[i+1] 72 | } else { 73 | m.Captures = append(m.Captures, "") 74 | } 75 | } 76 | 77 | if len(m.Captures) == 0 || m.Left == -1 { 78 | return nil 79 | } 80 | 81 | m.Text = text[m.Left:m.Right] 82 | return m 83 | } 84 | -------------------------------------------------------------------------------- /rules/sort.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | type MatchByIndex []*Match 4 | 5 | func (m MatchByIndex) Len() int { 6 | return len(m) 7 | } 8 | 9 | func (m MatchByIndex) Swap(i, j int) { 10 | m[i], m[j] = m[j], m[i] 11 | } 12 | 13 | func (m MatchByIndex) Less(i, j int) bool { 14 | return m[i].Left < m[j].Left 15 | } 16 | 17 | type MatchByOrder []*Match 18 | 19 | func (m MatchByOrder) Len() int { 20 | return len(m) 21 | } 22 | 23 | func (m MatchByOrder) Swap(i, j int) { 24 | m[i], m[j] = m[j], m[i] 25 | } 26 | 27 | func (m MatchByOrder) Less(i, j int) bool { 28 | return m[i].Order < m[j].Order 29 | } 30 | -------------------------------------------------------------------------------- /rules/zh/after_time.go: -------------------------------------------------------------------------------- 1 | package zh 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | ) 10 | 11 | /* 12 | 5/五 分钟后 13 | 5 小时后 14 | */ 15 | 16 | func AfterTime(s rules.Strategy) rules.Rule { 17 | return &rules.F{ 18 | RegExp: regexp.MustCompile("(?i)" + 19 | "((?:[0-9]{0,3}))?" + 20 | "(" + INTEGER_WORDS_PATTERN[3:] + "?" + "\\s*" + 21 | "(?:(分|分钟|小时|天|周|月)\\s*)" + 22 | "(后)" + 23 | "(?:\\W|$)", 24 | ), 25 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 26 | if c.Hour != nil && s != rules.Override { 27 | return false, nil 28 | } 29 | duration, _ := strconv.Atoi(m.Captures[0]) 30 | 31 | if d, exist := INTEGER_WORDS[compressStr(m.Captures[1])]; exist { 32 | duration = d 33 | } 34 | if m.Captures[1] == "半" && m.Captures[2] == "小时" { 35 | c.Duration = time.Minute * time.Duration(30) 36 | return true, nil 37 | } 38 | 39 | switch m.Captures[2] { 40 | case "分钟", "分": 41 | c.Duration = time.Minute * time.Duration(duration) 42 | case "小时": 43 | c.Duration = time.Hour * time.Duration(duration) 44 | case "天": 45 | c.Duration = time.Hour * 24 * time.Duration(duration) 46 | case "周": 47 | c.Duration = time.Hour * 24 * 7 * time.Duration(duration) 48 | case "月": 49 | _, _ = c.Time(time.Now().AddDate(0, duration, 0)) 50 | } 51 | 52 | return true, nil 53 | }, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rules/zh/casual_date.go: -------------------------------------------------------------------------------- 1 | package zh 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/AlekSi/pointer" 10 | "github.com/olebedev/when/rules" 11 | ) 12 | 13 | func CasualDate(s rules.Strategy) rules.Rule { 14 | overwrite := s == rules.Override 15 | 16 | return &rules.F{ 17 | RegExp: regexp.MustCompile("(?i)" + 18 | "(大前|前|昨|今天|今|明|大后|后|下下|下|上|上上)" + "(天|月|个月|年|儿)" + 19 | "(1[0-9]|2[0-9]|3[0-1]|[1-9]|" + DAY_WORDS_PATTERN + ")?" + "(?:\\s*)?" + 20 | "(日|号)?" + 21 | "", 22 | // "(?:\\W|$)", 23 | ), 24 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 25 | lower := compressStr(strings.TrimSpace(m.String())) 26 | 27 | switch { 28 | case strings.Contains(lower, "号"), strings.Contains(lower, "日"): 29 | day, _ := strconv.Atoi(m.Captures[2]) 30 | c.Day = pointer.ToInt(day) 31 | } 32 | 33 | switch { 34 | 35 | case strings.Contains(lower, "后年"): 36 | c.Year = pointer.ToInt(ref.Year() + 2) 37 | case strings.Contains(lower, "明年"): 38 | c.Year = pointer.ToInt(ref.Year() + 1) 39 | case strings.Contains(lower, "下下"): 40 | monthInt := int(ref.Month()) + 2 41 | c.Month = pointer.ToInt(monthInt) 42 | case strings.Contains(lower, "下月"), strings.Contains(lower, "下个月"): 43 | monthInt := int(ref.Month()) + 1 44 | c.Month = pointer.ToInt(monthInt) 45 | case strings.Contains(lower, "上上"): 46 | monthInt := int(ref.Month()) - 2 47 | c.Month = pointer.ToInt(monthInt) 48 | case strings.Contains(lower, "上月"), strings.Contains(lower, "上个月"): 49 | monthInt := int(ref.Month()) - 1 50 | c.Month = pointer.ToInt(monthInt) 51 | case strings.Contains(lower, "今晚"), strings.Contains(lower, "晚上"): 52 | if c.Hour == nil && c.Minute == nil || overwrite { 53 | c.Hour = pointer.ToInt(22) 54 | c.Minute = pointer.ToInt(0) 55 | } 56 | case strings.Contains(lower, "今天"), strings.Contains(lower, "今儿"): 57 | // c.Hour = pointer.ToInt(18) 58 | case strings.Contains(lower, "明天"), strings.Contains(lower, "明儿"): 59 | if c.Duration == 0 || overwrite { 60 | c.Duration += time.Hour * 24 61 | } 62 | case strings.Contains(lower, "昨天"): 63 | if c.Duration == 0 || overwrite { 64 | c.Duration -= time.Hour * 24 65 | } 66 | case strings.Contains(lower, "大前天"): 67 | if c.Duration == 0 || overwrite { 68 | c.Duration -= time.Hour * 24 * 3 69 | } 70 | case strings.Contains(lower, "前天"): 71 | if c.Duration == 0 || overwrite { 72 | c.Duration -= time.Hour * 24 * 2 73 | } 74 | case strings.Contains(lower, "昨晚"): 75 | if (c.Hour == nil && c.Duration == 0) || overwrite { 76 | c.Hour = pointer.ToInt(23) 77 | c.Duration -= time.Hour * 24 78 | } 79 | case strings.Contains(lower, "大后天"): 80 | if c.Duration == 0 || overwrite { 81 | c.Duration += time.Hour * 24 * 3 82 | } 83 | case strings.Contains(lower, "后天"): 84 | if c.Duration == 0 || overwrite { 85 | c.Duration += time.Hour * 24 * 2 86 | } 87 | } 88 | 89 | return true, nil 90 | }, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /rules/zh/casual_date_test.go: -------------------------------------------------------------------------------- 1 | package zh_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/zh" 10 | ) 11 | 12 | func TestCasualDate(t *testing.T) { 13 | fixt := []Fixture{ 14 | {"后天中午", 0, "后天", (2 * 24) * time.Hour}, 15 | {"大后天中午", 0, "大后天", (3 * 24) * time.Hour}, 16 | {"昨天", 0, "昨天", (-1 * 24) * time.Hour}, 17 | {"前天", 0, "前天", (-2 * 24) * time.Hour}, 18 | {"大前天", 0, "大前天", (-3 * 24) * time.Hour}, 19 | {"下月", 0, "下月", (31 * 24) * time.Hour}, 20 | {"下个月", 0, "下个月", (31 * 24) * time.Hour}, 21 | {"下下月", 0, "下下月", (31*24 + 30*24) * time.Hour}, 22 | {"下下个月", 0, "下下个月", (31*24 + 30*24) * time.Hour}, 23 | {"明年", 0, "明年", (365 * 24) * time.Hour}, 24 | {"后年", 0, "后年", now.AddDate(2, 0, 0).Sub(now)}, 25 | {"下月6号", 0, "下月6号", 552 * time.Hour}, 26 | } 27 | 28 | w := when.New(nil) 29 | 30 | w.Add(zh.CasualDate(rules.Override)) 31 | 32 | ApplyFixtures(t, "zh.TestCasualDate", w, fixt) 33 | } 34 | 35 | /* 36 | (([1-9](?:月|-|/|\.|))|1[0-2])\s*(月|-|/|\.|)\s*([1-9]|1[0-9]|2[0-9]|3[0-1])\s*(日|号)?(?:\W|$) 37 | */ 38 | -------------------------------------------------------------------------------- /rules/zh/casual_time.go: -------------------------------------------------------------------------------- 1 | package zh 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/AlekSi/pointer" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func CasualTime(s rules.Strategy) rules.Rule { 13 | overwrite := s == rules.Override 14 | 15 | return &rules.F{ 16 | RegExp: regexp.MustCompile(`(?i)(?:\W|^)((今天)?\s*(早晨|下午|傍晚|中午|晚上))`), 17 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 18 | 19 | lower := strings.ToLower(strings.TrimSpace(m.String())) 20 | 21 | if (c.Hour != nil || c.Minute != nil) && !overwrite { 22 | return false, nil 23 | } 24 | 25 | switch { 26 | case strings.Contains(lower, "晚上"): 27 | if o.Evening != 0 { 28 | c.Hour = &o.Evening 29 | } else { 30 | c.Hour = pointer.ToInt(20) 31 | } 32 | c.Minute = pointer.ToInt(0) 33 | case strings.Contains(lower, "下午"): 34 | if o.Afternoon != 0 { 35 | c.Hour = &o.Afternoon 36 | } else { 37 | c.Hour = pointer.ToInt(15) 38 | } 39 | c.Minute = pointer.ToInt(0) 40 | case strings.Contains(lower, "傍晚"): 41 | if o.Evening != 0 { 42 | c.Hour = &o.Evening 43 | } else { 44 | c.Hour = pointer.ToInt(18) 45 | } 46 | c.Minute = pointer.ToInt(0) 47 | case strings.Contains(lower, "早晨"): 48 | if o.Morning != 0 { 49 | c.Hour = &o.Morning 50 | } else { 51 | c.Hour = pointer.ToInt(8) 52 | } 53 | c.Minute = pointer.ToInt(0) 54 | case strings.Contains(lower, "中午"): 55 | if o.Noon != 0 { 56 | c.Hour = &o.Noon 57 | } else { 58 | c.Hour = pointer.ToInt(12) 59 | } 60 | c.Minute = pointer.ToInt(0) 61 | } 62 | 63 | return true, nil 64 | }, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /rules/zh/exact_month_date.go: -------------------------------------------------------------------------------- 1 | package zh 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | ) 10 | 11 | /* 12 | 规则名称:精确到月份的日期 13 | */ 14 | 15 | func ExactMonthDate(s rules.Strategy) rules.Rule { 16 | overwrite := s == rules.Override 17 | 18 | return &rules.F{ 19 | RegExp: regexp.MustCompile("" + 20 | "(?:\\b|^)" + // can't use \W here due to Chinese characters 21 | "(?:" + 22 | "(1[0-2]|[1-9])" + "\\s*" + "(?:-|/|\\.)" + "\\s*" + "(1[0-9]|2[0-9]|3[0-1]|[1-9])" + 23 | "|" + 24 | "(?:" + 25 | "(1[0-2]|[1-9]|" + MON_WORDS_PATTERN + ")" + "\\s*" + 26 | "(月)" + "\\s*" + 27 | ")?" + 28 | "(?:" + 29 | "(1[0-9]|2[0-9]|3[0-1]|[1-9]|" + DAY_WORDS_PATTERN + ")" + "\\s*" + 30 | "(日|号)" + 31 | ")?" + 32 | ")", 33 | ), 34 | 35 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 36 | _ = overwrite 37 | 38 | // the default value of month is the current month, and the default 39 | // value of day is the first day of the month, so that we can handle 40 | // cases like "4月" (Apr 1st) and "12号" (12th this month) 41 | var monInt = int(ref.Month()) 42 | var dayInt = 1 43 | var exist bool 44 | 45 | if m.Captures[0] == "" && m.Captures[2] == "" && m.Captures[4] == "" { 46 | return false, nil 47 | } 48 | 49 | if m.Captures[2] != "" { 50 | monInt, exist = MON_WORDS[compressStr(m.Captures[2])] 51 | if !exist { 52 | mon, err := strconv.Atoi(m.Captures[2]) 53 | if err != nil { 54 | return false, nil 55 | } 56 | monInt = mon 57 | } 58 | } 59 | 60 | if m.Captures[4] != "" { 61 | dayInt, exist = DAY_WORDS[compressStr(m.Captures[4])] 62 | if !exist { 63 | day, err := strconv.Atoi(m.Captures[4]) 64 | if err != nil { 65 | return false, nil 66 | } 67 | dayInt = day 68 | } 69 | } 70 | 71 | if m.Captures[0] != "" && m.Captures[1] != "" { 72 | mon, err := strconv.Atoi(m.Captures[0]) 73 | if err != nil { 74 | return false, nil 75 | } 76 | day, err := strconv.Atoi(m.Captures[1]) 77 | if err != nil { 78 | return false, nil 79 | } 80 | monInt = mon 81 | dayInt = day 82 | } 83 | 84 | c.Month = &monInt 85 | c.Day = &dayInt 86 | 87 | return true, nil 88 | }, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /rules/zh/exact_month_test.go: -------------------------------------------------------------------------------- 1 | package zh_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/zh" 10 | ) 11 | 12 | func TestExactMonthDate(t *testing.T) { 13 | // current is Monday 14 | fixt := []Fixture{ 15 | {"4月1日", 0, "4月1日", (18 * 24) * time.Hour}, 16 | {"4月2日", 0, "4月2日", (19 * 24) * time.Hour}, 17 | {"4月 2日", 0, "4月 2日", (19 * 24) * time.Hour}, 18 | {"4 月 2 日", 0, "4 月 2 日", (19 * 24) * time.Hour}, 19 | {"四月一日", 0, "四月一日", (18 * 24) * time.Hour}, 20 | {"四月1日", 0, "四月1日", (18 * 24) * time.Hour}, 21 | {"四月", 0, "四月", (18 * 24) * time.Hour}, 22 | {"十一月一日", 0, "十一月一日", 5568 * time.Hour}, 23 | {"四月三十日", 0, "四月三十日", 1128 * time.Hour}, 24 | {"4月30日", 0, "4月30日", 1128 * time.Hour}, 25 | {"5月1号", 0, "5月1号", 1152 * time.Hour}, 26 | {"5/1", 0, "5/1", 1152 * time.Hour}, 27 | {"5月1日", 0, "5月1日", 1152 * time.Hour}, 28 | {"五月", 0, "五月", 1152 * time.Hour}, 29 | {"12号", 0, "12号", (-2 * 24) * time.Hour}, 30 | } 31 | 32 | w := when.New(nil) 33 | 34 | w.Add(zh.ExactMonthDate(rules.Override)) 35 | 36 | ApplyFixtures(t, "zh.ExactMonthDate", w, fixt) 37 | } 38 | 39 | func TestExactMonthDateNil(t *testing.T) { 40 | fixt := []Fixture{ 41 | {"41", 0, "", (18 * 24) * time.Hour}, 42 | } 43 | 44 | w := when.New(nil) 45 | 46 | w.Add(zh.ExactMonthDate(rules.Override)) 47 | ApplyFixturesNil(t, "zh.ExactMonthDate", w, fixt) 48 | 49 | } 50 | 51 | /* 52 | (([1-9](?:月|-|/|\.|))|1[0-2])\s*(月|-|/|\.|)\s*([1-9]|1[0-9]|2[0-9]|3[0-1])\s*(日|号)?(?:\W|$) 53 | */ 54 | -------------------------------------------------------------------------------- /rules/zh/hour_minute.go: -------------------------------------------------------------------------------- 1 | package zh 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | ) 10 | 11 | /* 12 | "上午 5点" 13 | "上午 5 点" 14 | "下午 3点" 15 | "下午 3 点" 16 | "下午 3点半" 17 | "下午 3点30" 18 | "下午 3:30" 19 | "下午 3:30" 20 | "下午 三点半" 21 | */ 22 | 23 | func HourMinute(s rules.Strategy) rules.Rule { 24 | return &rules.F{ 25 | RegExp: regexp.MustCompile("(?i)" + 26 | "(?:(凌\\s*晨|早\\s*晨|早\\s*上|上\\s*午|下\\s*午|晚\\s*上|今晚)?\\s*)" + 27 | "((?:[0-1]{0,1}[0-9])|(?:2[0-3]))?" + "(?:\\s*)" + 28 | "(" + INTEGER_WORDS_PATTERN[3:] + "?" + 29 | "(\\:|:|\\-|点)" + 30 | "((?:[0-5][0-9]))?" + 31 | "(" + INTEGER_WORDS_PATTERN + "+)?" + 32 | "(?:\\W|$)"), 33 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 34 | if (c.Hour != nil || c.Minute != nil) && s != rules.Override { 35 | return false, nil 36 | } 37 | 38 | hour, exist := INTEGER_WORDS[m.Captures[2]] // 中文 39 | if !exist { 40 | hour, _ = strconv.Atoi(m.Captures[1]) 41 | } 42 | 43 | if hour > 24 { 44 | return false, nil 45 | } 46 | 47 | minutes, exist := INTEGER_WORDS[m.Captures[5]] 48 | if !exist { 49 | minutes, _ = strconv.Atoi(m.Captures[4]) 50 | } 51 | 52 | if minutes > 59 { 53 | return false, nil 54 | } 55 | c.Minute = &minutes 56 | 57 | lower := compressStr(m.Captures[0]) 58 | switch lower { 59 | case "上午", "凌晨", "早晨", "早上": 60 | c.Hour = &hour 61 | case "下午", "晚上", "今晚": 62 | if hour < 12 { 63 | hour += 12 64 | } 65 | c.Hour = &hour 66 | case "": 67 | if hour > 23 { 68 | return false, nil 69 | } 70 | c.Hour = &hour 71 | 72 | } 73 | return true, nil 74 | }, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /rules/zh/hour_minute_test.go: -------------------------------------------------------------------------------- 1 | package zh_test 2 | 3 | import ( 4 | "github.com/olebedev/when/rules/zh" 5 | "testing" 6 | "time" 7 | 8 | "github.com/olebedev/when" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func TestHourMinute(t *testing.T) { 13 | // current is Monday 14 | fixt := []Fixture{ 15 | {"上午 11:30", 0, "上午 11:30", 11*time.Hour + 30*time.Minute}, 16 | {"下午 3:30", 0, "下午 3:30", 15*time.Hour + 30*time.Minute}, 17 | {"下午 3点半", 0, "下午 3点半", 15*time.Hour + 30*time.Minute}, 18 | {"凌晨 3点半", 0, "凌晨 3点半", 3*time.Hour + 30*time.Minute}, 19 | {"晚上8:00", 0, "晚上8:00", 20*time.Hour + 0*time.Minute}, 20 | {"晚上9:32", 0, "晚上9:32", 21*time.Hour + 32*time.Minute}, 21 | {"晚 上 8:00", 0, "晚 上 8:00", 20*time.Hour + 0*time.Minute}, 22 | {"晚上 8 点干啥去", 0, "晚上 8 点", 20*time.Hour + 0*time.Minute}, 23 | {"他俩凌晨 3点去散步太可怕了", 6, "凌晨 3点", 3*time.Hour + 0*time.Minute}, 24 | {"早晨八点一刻", 0, "早晨八点一刻", 8*time.Hour + 15*time.Minute}, 25 | {"早上八点半", 0, "早上八点半", 8*time.Hour + 30*time.Minute}, 26 | {"今晚八点", 0, "今晚八点", 20 * time.Hour}, 27 | {"今晚八点半", 0, "今晚八点半", 20*time.Hour + 30*time.Minute}, 28 | } 29 | 30 | w := when.New(nil) 31 | 32 | w.Add(zh.HourMinute(rules.Override)) 33 | 34 | ApplyFixtures(t, "zh.HourMinute", w, fixt) 35 | } 36 | -------------------------------------------------------------------------------- /rules/zh/tradition_hour.go: -------------------------------------------------------------------------------- 1 | package zh 2 | 3 | import ( 4 | "regexp" 5 | "time" 6 | 7 | "github.com/olebedev/when/rules" 8 | ) 9 | 10 | /* 11 | 子时 23:00 - 01:00 12 | 丑时 01:00 - 03:00 13 | 寅时 03:00 - 05:00 14 | 卯时 05:00 - 07:00 15 | 辰时 07:00 - 09:00 16 | 巳时 09:00 - 11:00 17 | 午时 11:00 - 13:00 18 | 未时 13:00 - 15:00 19 | 申时 15:00 - 17:00 20 | 酉时 17:00 - 19:00 21 | 戌时 19:00 - 21:00 22 | 亥时 21:00 - 23:00 23 | */ 24 | 25 | func TraditionHour(s rules.Strategy) rules.Rule { 26 | return &rules.F{ 27 | RegExp: regexp.MustCompile("" + 28 | "(?:(子\\s?时|丑\\s?时|寅\\s?时|卯\\s?时|辰\\s?时|巳\\s?时|午\\s?时|未\\s?时|申\\s?时|酉\\s?时|戌\\s?时|亥\\s?时))\\s?" + 29 | "(?:(一\\s?刻|二\\s?刻|两\\s?刻|三\\s?刻|四\\s?刻|五\\s?刻|六\\s?刻|七\\s?刻|1\\s?刻|2\\s?刻|3\\s?刻|4\\s?刻|5\\s?刻|6\\s?刻|7\\s?刻))?", 30 | ), 31 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 32 | if c.Hour != nil && s != rules.Override { 33 | return false, nil 34 | } 35 | hour, exist := TRADITION_HOUR_WORDS[compressStr(m.Captures[0])] 36 | if !exist { 37 | return false, nil 38 | } 39 | c.Hour = &hour 40 | zero := 0 41 | c.Minute = &zero 42 | if minute, exist := TRADITION_MINUTE_WORDS[compressStr(m.Captures[1])]; exist { 43 | if minute > 60 { 44 | hour := *c.Hour + 1 45 | c.Hour = &hour 46 | minute = minute - 60 47 | c.Minute = &minute 48 | } else { 49 | c.Minute = &minute 50 | } 51 | } 52 | return true, nil 53 | }, 54 | } 55 | } 56 | 57 | func compressStr(str string) string { 58 | if str == "" { 59 | return "" 60 | } 61 | reg := regexp.MustCompile(`\s+`) 62 | return reg.ReplaceAllString(str, "") 63 | } 64 | -------------------------------------------------------------------------------- /rules/zh/tradition_hour_test.go: -------------------------------------------------------------------------------- 1 | package zh_test 2 | 3 | import ( 4 | "github.com/olebedev/when/rules/zh" 5 | "testing" 6 | "time" 7 | 8 | "github.com/olebedev/when" 9 | "github.com/olebedev/when/rules" 10 | ) 11 | 12 | func TestTraditionHour(t *testing.T) { 13 | // current is Monday 14 | fixt := []Fixture{ 15 | {"午 时123", 0, "午 时", 11 * time.Hour}, 16 | {"子时", 0, "子时", 23 * time.Hour}, 17 | {"午时太阳正好", 0, "午时", 11 * time.Hour}, 18 | {"我们在酉时喝一杯吧", 9, "酉时", 17 * time.Hour}, 19 | {"午时三刻问斩", 0, "午时三刻", 11*time.Hour + 45*time.Minute}, 20 | {"午时四刻吃饭", 0, "午时四刻", 12 * time.Hour}, 21 | {"戌时1刻", 0, "戌时1刻", 19*time.Hour + 15*time.Minute}, 22 | } 23 | 24 | w := when.New(nil) 25 | 26 | w.Add(zh.TraditionHour(rules.Override)) 27 | 28 | ApplyFixtures(t, "zh.TraditionHour", w, fixt) 29 | } 30 | -------------------------------------------------------------------------------- /rules/zh/weedkay.go: -------------------------------------------------------------------------------- 1 | package zh 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "time" 7 | 8 | "github.com/olebedev/when/rules" 9 | ) 10 | 11 | func Weekday(s rules.Strategy) rules.Rule { 12 | overwrite := s == rules.Override 13 | 14 | return &rules.F{ 15 | RegExp: regexp.MustCompile("(?i)" + 16 | "(?:(本|这|下|上|这个|下个|上个|下下)\\s*)?" + 17 | "(?:(周|礼拜|星期)\\s*)" + 18 | "(1|2|3|4|5|6|天|一|二|三|四|五|六|日)" + 19 | "(?:\\W|$)", 20 | ), 21 | 22 | Applier: func(m *rules.Match, c *rules.Context, o *rules.Options, ref time.Time) (bool, error) { 23 | _ = overwrite 24 | 25 | if strings.TrimSpace(m.Captures[1]) == "" { 26 | return false, nil 27 | } 28 | 29 | day := strings.ToLower(strings.TrimSpace(m.Captures[2])) 30 | norm := strings.ToLower(strings.TrimSpace(m.Captures[0])) 31 | if norm == "" { 32 | norm = "本" 33 | } 34 | dayInt, ok := WEEKDAY_OFFSET[day] 35 | if !ok { 36 | return false, nil 37 | } 38 | 39 | if c.Duration != 0 && !overwrite { 40 | return false, nil 41 | } 42 | 43 | // Switch: 44 | switch { 45 | case strings.Contains(norm, "上"): 46 | diff := int(ref.Weekday()) - dayInt 47 | c.Duration = -time.Duration(7+diff) * 24 * time.Hour 48 | case strings.Contains(norm, "下下"): 49 | diff := dayInt - int(ref.Weekday()) 50 | c.Duration = time.Duration(7+7+diff) * 24 * time.Hour 51 | case strings.Contains(norm, "下"): 52 | diff := dayInt - int(ref.Weekday()) 53 | c.Duration = time.Duration(7+diff) * 24 * time.Hour 54 | case strings.Contains(norm, "本") || strings.Contains(norm, "这"): 55 | if int(ref.Weekday()) < dayInt { 56 | diff := dayInt - int(ref.Weekday()) 57 | if diff > 0 { 58 | c.Duration = time.Duration(diff*24) * time.Hour 59 | } else if diff < 0 { 60 | c.Duration = time.Duration(7+diff) * 24 * time.Hour 61 | } else { 62 | c.Duration = 7 * 24 * time.Hour 63 | } 64 | } else if int(ref.Weekday()) > dayInt { 65 | diff := int(ref.Weekday()) - dayInt 66 | if diff > 0 { 67 | c.Duration = -time.Duration(diff*24) * time.Hour 68 | } else if diff < 0 { 69 | c.Duration = -time.Duration(7+diff) * 24 * time.Hour 70 | } else { 71 | c.Duration = -(7 * 24 * time.Hour) 72 | } 73 | } 74 | } 75 | 76 | return true, nil 77 | }, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /rules/zh/weekday_test.go: -------------------------------------------------------------------------------- 1 | package zh_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/olebedev/when" 8 | "github.com/olebedev/when/rules" 9 | "github.com/olebedev/when/rules/zh" 10 | ) 11 | 12 | func TestWeekday(t *testing.T) { 13 | // current is Monday 14 | fixt := []Fixture{ 15 | {"和你下周一吃饭", 6, "下周一", 7 * 24 * time.Hour}, 16 | {"下星期三", 0, "下星期三", 9 * 24 * time.Hour}, 17 | {"和小西本周三一起打羽毛球", 9, "本周三", 2 * 24 * time.Hour}, 18 | {"这周三", 0, "这周三", 2 * 24 * time.Hour}, 19 | {"这礼拜四浇花", 0, "这礼拜四", 3 * 24 * time.Hour}, 20 | {"这星期 4", 0, "这星期 4", 3 * 24 * time.Hour}, 21 | {"和李星期这星期 4喝茶", 12, "这星期 4", 3 * 24 * time.Hour}, 22 | {"周日", 0, "周日", 6 * 24 * time.Hour}, 23 | {"下周日", 0, "下周日", (6 + 7) * 24 * time.Hour}, 24 | {"2下周天", 1, "下周天", (6 + 7) * 24 * time.Hour}, 25 | {"上周三", 0, "上周三", -5 * 24 * time.Hour}, 26 | {"下个周三", 0, "下个周三", (7 + 2) * 24 * time.Hour}, 27 | {"1下个礼拜 3", 1, "下个礼拜 3", (7 + 2) * 24 * time.Hour}, 28 | {"下下礼拜 3", 0, "下下礼拜 3", (7 + 7 + 2) * 24 * time.Hour}, 29 | } 30 | 31 | w := when.New(nil) 32 | 33 | w.Add(zh.Weekday(rules.Override)) 34 | 35 | ApplyFixtures(t, "zh.Weekday", w, fixt) 36 | } 37 | -------------------------------------------------------------------------------- /rules/zh/zh.go: -------------------------------------------------------------------------------- 1 | package zh 2 | 3 | import "github.com/olebedev/when/rules" 4 | 5 | var All = []rules.Rule{ 6 | Weekday(rules.Override), 7 | CasualDate(rules.Override), 8 | CasualTime(rules.Override), 9 | HourMinute(rules.Override), 10 | ExactMonthDate(rules.Override), 11 | TraditionHour(rules.Override), 12 | AfterTime(rules.Override), 13 | } 14 | 15 | var WEEKDAY_OFFSET = map[string]int{ 16 | "天": 7, 17 | "一": 1, 18 | "二": 2, 19 | "三": 3, 20 | "四": 4, 21 | "五": 5, 22 | "六": 6, 23 | "1": 1, 24 | "2": 2, 25 | "3": 3, 26 | "4": 4, 27 | "5": 5, 28 | "6": 6, 29 | "日": 7, 30 | } 31 | 32 | var INTEGER_WORDS = map[string]int{ 33 | "零一": 1, 34 | "零二": 2, 35 | "零三": 3, 36 | "零四": 4, 37 | "零五": 5, 38 | "零六": 6, 39 | "零七": 7, 40 | "零八": 8, 41 | "零九": 9, 42 | "一": 1, 43 | "二": 2, 44 | "两": 2, 45 | "三": 3, 46 | "四": 4, 47 | "五": 5, 48 | "六": 6, 49 | "七": 7, 50 | "八": 8, 51 | "九": 9, 52 | "十": 10, 53 | "十一": 11, 54 | "十二": 12, 55 | "十三": 13, 56 | "十四": 14, 57 | "十五": 15, 58 | "十六": 16, 59 | "十七": 17, 60 | "十八": 18, 61 | "十九": 19, 62 | "二十": 20, 63 | "二十一": 21, 64 | "二十二": 22, 65 | "二十三": 23, 66 | "二十五": 25, 67 | "二十六": 26, 68 | "二十七": 27, 69 | "二十八": 28, 70 | "二十九": 29, 71 | "三十": 30, 72 | "三十一": 31, 73 | "三十二": 32, 74 | "三十三": 33, 75 | "三十四": 34, 76 | "三十五": 35, 77 | "三十六": 36, 78 | "三十七": 37, 79 | "三十八": 38, 80 | "三十九": 39, 81 | "四十": 40, 82 | "四十一": 41, 83 | "四十二": 42, 84 | "四十三": 43, 85 | "四十四": 44, 86 | "四十五": 45, 87 | "四十六": 46, 88 | "四十七": 47, 89 | "四十八": 48, 90 | "四十九": 49, 91 | "五十": 50, 92 | "五十一": 51, 93 | "五十二": 52, 94 | "五十三": 53, 95 | "五十四": 54, 96 | "五十五": 55, 97 | "五十六": 56, 98 | "五十七": 57, 99 | "五十八": 58, 100 | "五十九": 59, 101 | "零": 0, 102 | "半": 30, 103 | "一刻": 15, 104 | } 105 | var INTEGER_WORDS_PATTERN = `(?:一刻|半|零一|零二|零三|零四|零五|零六|零七|零八|零九|一|两|二|三|四|五|六|七|八|九|十|十一|十二|十三|十四|十五|十六|十七|十八|十九|二十|二十一|二十二|二十三|二十五|二十六|二十七|二十八|二十九|三十|三十一|三十二|三十三|三十四|三十五|三十六|三十七|三十八|三十九|四十|四十一|四十二|四十三|四十四|四十五|四十六|四十七|四十八|四十九|五十|五十一|五十二|五十三|五十四|五十五|五十六|五十七|五十八|五十九|零)` 106 | 107 | var TRADITION_HOUR_WORDS = map[string]int{ 108 | "子时": 23, 109 | "丑时": 1, 110 | "寅时": 3, 111 | "卯时": 5, 112 | "辰时": 7, 113 | "巳时": 9, 114 | "午时": 11, 115 | "未时": 13, 116 | "申时": 15, 117 | "酉时": 17, 118 | "戌时": 19, 119 | "亥时": 21, 120 | } 121 | var TRADITION_MINUTE_WORDS = map[string]int{ 122 | "一刻": 15, 123 | "两刻": 30, 124 | "二刻": 30, 125 | "三刻": 45, 126 | "四刻": 60, 127 | "五刻": 75, 128 | "六刻": 90, 129 | "七刻": 105, 130 | "八刻": 120, 131 | "1刻": 15, 132 | "2刻": 30, 133 | "3刻": 45, 134 | "4刻": 60, 135 | "5刻": 75, 136 | "6刻": 90, 137 | "7刻": 105, 138 | "8刻": 120, 139 | } 140 | 141 | var MON_WORDS = map[string]int{ 142 | "一": 1, 143 | "二": 2, 144 | "三": 3, 145 | "四": 4, 146 | "五": 5, 147 | "六": 6, 148 | "七": 7, 149 | "八": 8, 150 | "九": 9, 151 | "十": 10, 152 | "十一": 11, 153 | "十二": 12, 154 | } 155 | 156 | var MON_WORDS_PATTERN = `十一|十二|一|二|三|四|五|六|七|八|九|十` 157 | 158 | var DAY_WORDS = map[string]int{ 159 | "一": 1, 160 | "二": 2, 161 | "四": 4, 162 | "五": 5, 163 | "六": 6, 164 | "七": 7, 165 | "八": 8, 166 | "九": 9, 167 | "十一": 11, 168 | "十二": 12, 169 | "十三": 13, 170 | "十四": 14, 171 | "十五": 15, 172 | "十六": 16, 173 | "十七": 17, 174 | "十八": 18, 175 | "十九": 19, 176 | "十": 10, 177 | "二十一": 21, 178 | "二十二": 22, 179 | "二十三": 23, 180 | "二十五": 25, 181 | "二十六": 26, 182 | "二十七": 27, 183 | "二十八": 28, 184 | "二十九": 29, 185 | "二十": 20, 186 | "三十一": 31, 187 | "三十": 30, 188 | "三": 3, 189 | } 190 | var DAY_WORDS_PATTERN = `一|二|四|五|六|七|八|九|十一|十二|十三|十四|十五|十六|十七|十八|十九|十|二十一|二十二|二十三|二十五|二十六|二十七|二十八|二十九|二十|三十一|三十|三` 191 | -------------------------------------------------------------------------------- /rules/zh/zh_test.go: -------------------------------------------------------------------------------- 1 | package zh_test 2 | 3 | import ( 4 | "github.com/olebedev/when" 5 | "github.com/stretchr/testify/require" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var now = time.Date(2022, time.March, 14, 0, 0, 0, 0, time.UTC) 11 | 12 | type Fixture struct { 13 | Text string 14 | Index int 15 | Phrase string 16 | Diff time.Duration 17 | } 18 | 19 | func ApplyFixtures(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 20 | for i, f := range fixt { 21 | res, err := w.Parse(f.Text, now) 22 | require.Nil(t, err, "[%s] err #%d", name, i) 23 | require.NotNil(t, res, "[%s] res #%d", name, i) 24 | require.Equal(t, f.Index, res.Index, "[%s] index #%d", name, i) 25 | require.Equal(t, f.Phrase, res.Text, "[%s] text #%d", name, i) 26 | require.Equal(t, f.Diff, res.Time.Sub(now), "[%s] diff #%d", name, i) 27 | } 28 | } 29 | 30 | func ApplyFixturesNil(t *testing.T, name string, w *when.Parser, fixt []Fixture) { 31 | for i, f := range fixt { 32 | res, err := w.Parse(f.Text, now) 33 | require.Nil(t, err, "[%s] err #%d", name, i) 34 | require.Nil(t, res, "[%s] res #%d", name, i) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: golang 2 | build: 3 | steps: 4 | - setup-go-workspace 5 | - script: 6 | name: go get 7 | code: | 8 | go version 9 | go get -v -t -d ./... 10 | - script: 11 | name: go test 12 | code: | 13 | go test -v ./... 14 | -------------------------------------------------------------------------------- /when.go: -------------------------------------------------------------------------------- 1 | package when 2 | 3 | import ( 4 | "sort" 5 | "time" 6 | 7 | "github.com/olebedev/when/rules" 8 | "github.com/olebedev/when/rules/br" 9 | "github.com/olebedev/when/rules/common" 10 | "github.com/olebedev/when/rules/en" 11 | "github.com/olebedev/when/rules/nl" 12 | "github.com/olebedev/when/rules/ru" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // Parser is a struct which contains options 17 | // rules, and middlewares to call 18 | type Parser struct { 19 | options *rules.Options 20 | rules []rules.Rule 21 | middleware []func(string) (string, error) 22 | } 23 | 24 | // Result is a struct which contains parsing meta-info 25 | type Result struct { 26 | // Index is a start index 27 | Index int 28 | // Text is a text found and processed 29 | Text string 30 | // Source is input string 31 | Source string 32 | // Time is an output time 33 | Time time.Time 34 | } 35 | 36 | // Parse returns Result and error if any. If have not matches it returns nil, nil. 37 | func (p *Parser) Parse(text string, base time.Time) (*Result, error) { 38 | res := Result{ 39 | Source: text, 40 | Time: base, 41 | Index: -1, 42 | } 43 | 44 | if p.options == nil { 45 | p.options = defaultOptions 46 | } 47 | 48 | var err error 49 | // apply middlewares 50 | for _, b := range p.middleware { 51 | text, err = b(text) 52 | if err != nil { 53 | return nil, err 54 | } 55 | } 56 | 57 | // find all matches 58 | matches := make([]*rules.Match, 0) 59 | c := float64(0) 60 | for _, rule := range p.rules { 61 | r := rule.Find(text) 62 | if r != nil { 63 | r.Order = c 64 | c++ 65 | matches = append(matches, r) 66 | } 67 | } 68 | 69 | // not found 70 | if len(matches) == 0 { 71 | return nil, nil 72 | } 73 | 74 | // find a cluster 75 | sort.Sort(rules.MatchByIndex(matches)) 76 | 77 | // get borders of the matches 78 | end := matches[0].Right 79 | res.Index = matches[0].Left 80 | 81 | for i, m := range matches { 82 | if m.Left <= end+p.options.Distance { 83 | end = m.Right 84 | } else { 85 | matches = matches[:i] 86 | break 87 | } 88 | } 89 | 90 | res.Text = text[res.Index:end] 91 | 92 | // apply rules 93 | if p.options.MatchByOrder { 94 | sort.Sort(rules.MatchByOrder(matches)) 95 | } 96 | 97 | ctx := &rules.Context{Text: res.Text} 98 | applied := false 99 | for _, applier := range matches { 100 | ok, err := applier.Apply(ctx, p.options, res.Time) 101 | if err != nil { 102 | return nil, err 103 | } 104 | applied = ok || applied 105 | } 106 | 107 | if !applied { 108 | return nil, nil 109 | } 110 | 111 | res.Time, err = ctx.Time(res.Time) 112 | if err != nil { 113 | return nil, errors.Wrap(err, "bind context") 114 | } 115 | 116 | return &res, nil 117 | } 118 | 119 | // Add adds given rules to the main chain. 120 | func (p *Parser) Add(r ...rules.Rule) { 121 | p.rules = append(p.rules, r...) 122 | } 123 | 124 | // Use adds give functions to middlewares. 125 | func (p *Parser) Use(f ...func(string) (string, error)) { 126 | p.middleware = append(p.middleware, f...) 127 | } 128 | 129 | // SetOptions sets options object to use. 130 | func (p *Parser) SetOptions(o *rules.Options) { 131 | p.options = o 132 | } 133 | 134 | // New returns Parser initialised with given options. 135 | func New(o *rules.Options) *Parser { 136 | if o == nil { 137 | return &Parser{options: defaultOptions} 138 | } 139 | return &Parser{options: o} 140 | } 141 | 142 | // default options for internal usage 143 | var defaultOptions = &rules.Options{ 144 | Distance: 5, 145 | MatchByOrder: true, 146 | } 147 | 148 | // EN is a parser for English language 149 | var EN *Parser 150 | 151 | // RU is a parser for Russian language 152 | var RU *Parser 153 | 154 | // BR is a parser for Brazilian Portuguese language 155 | var BR *Parser 156 | 157 | // NL is a parser for Dutch language 158 | var NL *Parser 159 | 160 | func init() { 161 | EN = New(nil) 162 | EN.Add(en.All...) 163 | EN.Add(common.All...) 164 | 165 | RU = New(nil) 166 | RU.Add(ru.All...) 167 | RU.Add(common.All...) 168 | 169 | BR = New(nil) 170 | BR.Add(br.All...) 171 | BR.Add(common.All...) 172 | 173 | NL = New(nil) 174 | NL.Add(nl.All...) 175 | NL.Add(common.All...) 176 | } 177 | --------------------------------------------------------------------------------