├── .gitignore ├── .istanbul.yml ├── .travis.yml ├── AUTHORS ├── LICENSE ├── NOTICE ├── README.md ├── index.js ├── lib ├── ast-visitor.js ├── errors.js ├── moment-generator.js ├── moment-parser.js └── parser.pegjs ├── package.json ├── scripts └── peg.js └── test ├── api.spec.js ├── binary-expression-asts.spec.js ├── binary-expressions.spec.js ├── calendar-expression-asts.spec.js ├── calendar-expressions.spec.js ├── duration-expressions.spec.js ├── duration-literal-asts.spec.js ├── duration-literals.spec.js ├── moment-expressions.spec.js ├── moment-literal-asts.spec.js └── moment-literals.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib/parser.js 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | check: 2 | global: 3 | statements: 100 4 | lines: 100 5 | branches: 100 6 | functions: 100 7 | excludes: [] 8 | 9 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '5.0' 5 | - '4.0' 6 | - '0.12' 7 | - '0.10' 8 | 9 | before_script: 10 | - npm install 11 | 12 | script: 13 | - npm test 14 | - npm run -s check-coverage 15 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | AUTHORS 2 | ======= 3 | 4 | Will Welch 5 | Brian Kerezturi 6 | Michael Demmer 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Juttle 2 | Copyright 2015 Jut, Inc. and Will Welch 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # moment-parser 2 | 3 | [![Build Status](https://travis-ci.org/juttle/moment-parser.svg)](https://travis-ci.org/juttle/moment-parser) 4 | [![Join the chat at https://gitter.im/juttle/moment-parser](https://badges.gitter.im/juttle/moment-parser.svg)](https://gitter.im/juttle/moment-parser?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | Natural language time parser for moment.js strings 7 | 8 | ### Quick start 9 | 10 | ```npm install moment-parser``` 11 | 12 | ```js 13 | var parser = require('moment-parser'); 14 | 15 | // durations 16 | console.log(parser.parse('1 hour')); //{ type: 'MomentDuration', value: 1, unit: 'hour' } 17 | console.log(parser.parseDuration('2 months and 1 day')); // a momentjs duration of { days: 1, months: 2 } 18 | 19 | // moments 20 | console.log(parser.parse('2015-01-01')); //{ type: 'ISODateLiteral', value: '2015-01-01T00:00:00' } 21 | console.log(parser.parseMoment('day 5 of next month')); // a momentjs moment at the start of next month plus 4 days 22 | ``` 23 | 24 | 25 | [More Examples In Tests](test/) 26 | 27 | 28 | ### Running Unit Tests 29 | 30 | ```npm test``` 31 | 32 | 33 | ## More examples 34 | (proper grammar docs coming soon!) 35 | 36 | ### Absolute Moments 37 | 38 | All times are UTC0 unless a time zone is specified by appending it to 39 | an ISO-8601 string. ​ ​ 40 | 41 | - Midnight on a specific date (UTC0): 42 | ``` 43 | 2014-09-21 44 | ``` 45 | ​ 46 | - An [ISO-8601](http://en.wikipedia.org/wiki/ISO_8601) date and time, UTC0: 47 | ``` 48 | 2014-09-22T11:39:17.993 49 | ``` 50 | ​ 51 | - The same date and time, as Pacific Standard Time: 52 | ``` 53 | 2014-09-22T03:39:17.993-08:00 54 | ``` 55 | ​ 56 | ### Durations 57 | ​ 58 | - One second, minute, hour, day, week, and so on: 59 | ``` 60 | second 61 | minute 62 | hour 63 | day 64 | week 65 | month 66 | year 67 | ``` 68 | ​ 69 | Note that `month` and `year` are special "calendar" durations that do 70 | not have fixed length but instead advance whole months or years relative 71 | to a fixed moment. 72 | ​ 73 | - Abbreviations for 0 seconds, 1 minute, 2 hours, 3 days, 4 weeks, 5 74 | months, 6 years: 75 | ``` 76 | 0s 77 | 1m 78 | 2h 79 | 3d 80 | 4w 81 | 5M 82 | 6y 83 | ``` 84 | ​ 85 | - Two ways to write one hour and twenty-three minutes: 86 | ``` 87 | 1 hour and 23 minutes 88 | 01:23:00 89 | ``` 90 | ​ 91 | - Additional examples: 92 | ``` 93 | 1 second 94 | 20 minutes 95 | ``` 96 | ​ 97 | ### Relative Moments 98 | ​ 99 | - The moment at which the program started running: 100 | ``` 101 | now 102 | ``` 103 | ​ 104 | - Midnight yesterday, today, or tomorrow: 105 | ``` 106 | yesterday 107 | today 108 | tomorrow 109 | ``` 110 | ​ 111 | - Seven hours after midnight on the current day: 112 | ``` 113 | 07:00:00 after today 114 | ``` 115 | ​ 116 | - One minute from the start of the program's execution: 117 | ``` 118 | 1 minute from now 119 | ``` 120 | ​ 121 | - Shorter way to write `2 minutes from now`: 122 | ``` 123 | +2m 124 | ``` 125 | ​ 126 | - Shorter way to write `1 hour and 10 minutes before now`: 127 | ``` 128 | -01:10:00 129 | ``` 130 | ​ 131 | - Now minus 72 hours: 132 | ``` 133 | 3 days ago 134 | ``` 135 | ​ 136 | - Now minus 22 days: 137 | ``` 138 | 3 weeks and 1 day ago 139 | ``` 140 | ​ 141 | - Midnight of the first day of the current calendar month: 142 | ``` 143 | this month 144 | ``` 145 | ​ 146 | - Midnight of the first day of the previous calendar month: 147 | ``` 148 | last month 149 | ``` 150 | ​ 151 | - Midnight of the 20th day of the previous calendar month: 152 | ``` 153 | day 20 of last month 154 | ``` 155 | ​ 156 | - The minute in which the program began executing: 157 | ``` 158 | this minute 159 | ``` 160 | ​ 161 | - The next even hour after the start of the program's execution: 162 | ``` 163 | next hour 164 | ``` 165 | ​ 166 | - The day in which the program began executing: 167 | ``` 168 | this day 169 | today 170 | ``` 171 | ​ 172 | - The day before the day in which the program began executing: 173 | ``` 174 | last day 175 | yesterday 176 | ``` 177 | ​ 178 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/moment-parser'); 2 | -------------------------------------------------------------------------------- /lib/ast-visitor.js: -------------------------------------------------------------------------------- 1 | var Base = require('extendable-base'); 2 | 3 | /* 4 | * Base class for recursively walking the parse tree and optionally generating 5 | * output. 6 | * 7 | * A derived class should implement functions caled visit_ for each 8 | * node type in the grammar. 9 | */ 10 | var ASTVisitor = Base.extend({ 11 | visit: function(node) { 12 | var type = node.type; 13 | var visitor = this['visit_' + type]; 14 | return visitor.apply(this, [node]); 15 | } 16 | }); 17 | 18 | module.exports = ASTVisitor; 19 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | var parser = require('./parser'); 2 | 3 | function subclass(child, parent) { 4 | function ctor() { this.constructor = child; } 5 | ctor.prototype = parent.prototype; 6 | child.prototype = new ctor(); 7 | } 8 | 9 | function NotAMomentError(message) { 10 | this.name = "NotAMomentError"; 11 | this.message = message || "Not a moment"; 12 | } 13 | subclass(NotAMomentError, Error); 14 | 15 | function NotADurationError(message) { 16 | this.name = "NotADurationError"; 17 | this.message = message || "Not a duration"; 18 | } 19 | subclass(NotADurationError, Error); 20 | 21 | module.exports = { 22 | SyntaxError: parser.SyntaxError, 23 | NotAMomentError: NotAMomentError, 24 | NotADurationError: NotADurationError 25 | }; 26 | -------------------------------------------------------------------------------- /lib/moment-generator.js: -------------------------------------------------------------------------------- 1 | var ASTVisitor = require('./ast-visitor'); 2 | var parser = require('./parser'); 3 | var errors = require('./errors'); 4 | var moment = require('moment'); 5 | 6 | /* 7 | * AST visitor that generates a moment from a parse tree. 8 | */ 9 | var MomentGenerator = ASTVisitor.extend({ 10 | initialize: function(options) { 11 | options = options || {}; 12 | this.now = options.now || moment.utc(); 13 | this.beginning = options.beginning || moment.utc(-100 * 1000000 * 1000 * 24 * 3600); 14 | this.end = options.end || moment.utc(100 * 1000000 * 1000 * 24 * 3600); 15 | }, 16 | 17 | visit_ISODateLiteral: function(node) { 18 | return moment.utc(node.value); 19 | }, 20 | 21 | visit_UnixTimeLiteral: function(node) { 22 | return moment.utc(node.value * 1000); 23 | }, 24 | 25 | visit_NowLiteral: function(node) { 26 | return this.now.clone(); 27 | }, 28 | 29 | visit_BeginningLiteral: function(node) { 30 | return this.beginning.clone(); 31 | }, 32 | 33 | visit_EndLiteral: function(node) { 34 | return this.end.clone(); 35 | }, 36 | 37 | visit_YesterdayLiteral: function(node) { 38 | return this.now.clone().startOf('day').subtract(1, 'day'); 39 | }, 40 | 41 | visit_TodayLiteral: function(node) { 42 | return this.now.clone().startOf('day'); 43 | }, 44 | 45 | visit_TomorrowLiteral: function(node) { 46 | return this.now.clone().startOf('day').add(1, 'day'); 47 | }, 48 | 49 | visit_ForeverLiteral: function(node) { 50 | return moment.duration(Infinity); 51 | }, 52 | 53 | visit_MomentDuration: function(node) { 54 | return moment.duration(node.value, node.unit); 55 | }, 56 | 57 | visit_ISODurationLiteral: function(node) { 58 | return moment.duration(node.value); 59 | }, 60 | 61 | visit_BinaryExpression: function(node) { 62 | if (node.operator === '+') { 63 | return this.visit(node.left).add(this.visit(node.right)); 64 | } else { 65 | return this.visit(node.left).subtract(this.visit(node.right)); 66 | } 67 | }, 68 | 69 | visit_CalendarExpression: function(node) { 70 | var expr = this.visit(node.expression); 71 | if (node.direction === 'down') { 72 | return expr.clone().startOf(node.unit); 73 | } else { 74 | return expr.clone().endOf(node.unit); 75 | } 76 | }, 77 | 78 | visit_TimeSpan: function(node) { 79 | return moment.duration({ 80 | milliseconds: node.milliseconds, 81 | seconds: node.seconds, 82 | minutes: node.minutes, 83 | hours: node.hours, 84 | days: node.days, 85 | months: node.months 86 | }); 87 | } 88 | }); 89 | 90 | module.exports = MomentGenerator; 91 | -------------------------------------------------------------------------------- /lib/moment-parser.js: -------------------------------------------------------------------------------- 1 | var peg_parser = require('./parser'); 2 | var errors = require('./errors'); 3 | var MomentGenerator = require('./moment-generator'); 4 | var moment = require('moment'); 5 | 6 | var parser = { 7 | /* 8 | * Parse the given input into an AST. 9 | */ 10 | parse: peg_parser.parse, 11 | 12 | /* 13 | * Parse the given input into an AST and generate a moment object based on 14 | * the parse tree. 15 | * 16 | * To support expressions that are relative to a "current" time, options.now 17 | * may contain a moment instance for the current time. It defaults 18 | * to the time of execution. 19 | */ 20 | parseMoment: function(source, options) { 21 | var ast = parser.parse(source); 22 | return parser.generateMoment(ast, options); 23 | }, 24 | 25 | /* 26 | * Given an AST, generate a moment object based on the parse tree. 27 | * 28 | * To support expressions that are relative to a "current" time, options.now 29 | * may contain a moment instance for the current time. It defaults 30 | * to the time of execution. 31 | */ 32 | generateMoment: function(ast, options) { 33 | var m = new MomentGenerator(options).visit(ast); 34 | if (!moment.isMoment(m)) { 35 | throw new errors.NotAMomentError(); 36 | } 37 | return m; 38 | }, 39 | 40 | /* 41 | * Parse the given input into an AST and generate a duration object based on 42 | * the parse tree. 43 | */ 44 | parseDuration: function(source, options) { 45 | var ast = parser.parse(source); 46 | return parser.generateDuration(ast, options); 47 | }, 48 | 49 | /* 50 | * Given an AST, generate a duration object based on the parse tree. 51 | */ 52 | generateDuration: function(ast, options) { 53 | var duration = new MomentGenerator(options).visit(ast); 54 | if (moment.isMoment(duration)) { 55 | throw new errors.NotADurationError(); 56 | } 57 | return duration; 58 | }, 59 | 60 | SyntaxError: errors.SyntaxError, 61 | NotAMomentError: errors.NotAMomentError, 62 | NotADurationError: errors.NotADurationError 63 | }; 64 | 65 | module.exports = parser; 66 | -------------------------------------------------------------------------------- /lib/parser.pegjs: -------------------------------------------------------------------------------- 1 | start 2 | = MomentValue 3 | 4 | // TODO: Can this be factored out of both parsers and just be included in? 5 | 6 | SourceCharacter 7 | = . 8 | 9 | _ 10 | = WhiteSpace* 11 | 12 | WhiteSpace "whitespace" 13 | = [\t\v\f \u00A0\uFEFF] 14 | 15 | DecimalLiteral 16 | = parts:$(DecimalIntegerLiteral "." DecimalDigits?) { 17 | return parseFloat(parts); 18 | } 19 | / parts:$("." DecimalDigits) { return parseFloat(parts); } 20 | / parts:$(DecimalIntegerLiteral) { return parseFloat(parts); } 21 | 22 | DecimalIntegerLiteral 23 | = "0" 24 | / NonZeroDigit DecimalDigits? 25 | 26 | Integer 27 | = digits:$DecimalDigits { return parseInt(digits); } 28 | 29 | Integer2 30 | = $(DecimalDigit DecimalDigit) 31 | 32 | Integer4 33 | = $(DecimalDigit DecimalDigit DecimalDigit DecimalDigit) 34 | 35 | DecimalDigits 36 | = DecimalDigit+ 37 | 38 | DecimalDigit 39 | = [0-9] 40 | 41 | NonZeroDigit 42 | = [1-9] 43 | 44 | // *************************** // 45 | 46 | UnixTimeLiteral 47 | = s:DecimalLiteral !SourceCharacter { 48 | return { 49 | type: "UnixTimeLiteral", 50 | valueType: "moment", 51 | value: s 52 | } 53 | } 54 | 55 | ISOTZ 56 | = "Z" 57 | / $(("+" / "-") Integer2 ":" Integer2 ) 58 | / $(("+" / "-") Integer4 ) 59 | 60 | ISODate 61 | = $(Integer4 "-" Integer2 "-" Integer2) 62 | 63 | ISOTime 64 | = $(Integer2 ":" Integer2 ":" Integer2 ("." DecimalDigits)?) 65 | 66 | ISODuration 67 | = $("P" Integer "W") 68 | / $("P" (Integer "Y")? (Integer "M")? (Integer "D")? ("T" (Integer "H")? (Integer "M")? (Integer "S")?)?) 69 | 70 | ISODateLiteral 71 | = d:ISODate t:$("T" ISOTime)? z:$ISOTZ? { 72 | return { 73 | type: "ISODateLiteral", 74 | valueType: "moment", 75 | value: d + (t? t : "T00:00:00") + (z ? z: "") 76 | } 77 | } 78 | 79 | ISODurationLiteral 80 | = ISODuration { 81 | return { 82 | type: "ISODurationLiteral", 83 | valueType: "duration", 84 | value: text() 85 | } 86 | } 87 | 88 | NowLiteral 89 | = "now" { 90 | return { 91 | type: "NowLiteral", 92 | valueType: "moment" 93 | }; 94 | } 95 | 96 | BeginningLiteral 97 | = "beginning" { 98 | return { 99 | type: "BeginningLiteral", 100 | valueType: "moment" 101 | }; 102 | } 103 | 104 | 105 | EndLiteral 106 | = "end" { 107 | return { 108 | type: "EndLiteral", 109 | valueType: "moment" 110 | }; 111 | } 112 | 113 | MomentString 114 | = NowLiteral 115 | / BeginningLiteral 116 | / EndLiteral 117 | / ISODateLiteral 118 | / UnixTimeLiteral 119 | 120 | DurationUnit 121 | = string:DurationString "s" { 122 | return string; 123 | } 124 | / DurationString 125 | / DurationAbbrev 126 | 127 | DurationString 128 | = "millisecond" 129 | / "second" 130 | / "minute" 131 | / "hour" 132 | / "day" 133 | / "week" 134 | 135 | DurationAbbrev 136 | = "ms" 137 | / "s" 138 | / "m" 139 | / "h" 140 | / "d" 141 | / "w" 142 | 143 | CalendarUnit 144 | = string:CalendarString "s" { 145 | return string; 146 | } 147 | / CalendarString 148 | / CalendarAbbrev 149 | 150 | CalendarString 151 | = "day" 152 | / "week" 153 | / "month" 154 | / "year" 155 | 156 | CalendarAbbrev 157 | = "d" 158 | / "w" 159 | / "M" 160 | / "y" 161 | 162 | HumanDuration 163 | = num:Integer? _ unit:CalendarUnit { 164 | return { 165 | type: "MomentDuration", 166 | valueType: "duration", 167 | value: (num === null) ? 1 : num, 168 | unit: unit 169 | }; 170 | } 171 | / num:DecimalLiteral? _ unit: DurationUnit { 172 | num = (num === null) ? 1 : num; 173 | 174 | return { 175 | type: "MomentDuration", 176 | valueType: "duration", 177 | value: num, 178 | unit: unit 179 | }; 180 | } 181 | 182 | TimeSpan 183 | = months:(Integer "/")? days:(Integer ".")? hours:Integer2 ":" minutes:Integer2 ":" seconds:Integer2 ms:("." $(DecimalDigit DecimalDigit? DecimalDigit?))? { 184 | return { 185 | type: "TimeSpan", 186 | valueType: "duration", 187 | months: (months === null) ? 0 : months[0], 188 | days: (days === null) ? 0 : days[0], 189 | hours: parseInt(hours), 190 | minutes: parseInt(minutes), 191 | seconds: parseInt(seconds), 192 | milliseconds: (ms === null) ? 0 : parseInt(ms[1]) 193 | }; 194 | } 195 | 196 | CalendarOffset 197 | = "first" _ unit:CalendarUnit _ "of" _ expr:CalendarExpression { 198 | return { 199 | type: "CalendarExpression", 200 | valueType: "moment", 201 | direction: "down", 202 | unit: unit, 203 | expression: expr 204 | }; 205 | } 206 | / "final" _ unit:CalendarUnit _ "of" _ expr:CalendarExpression { 207 | return { 208 | type: "CalendarExpression", 209 | valueType: "moment", 210 | direction: "down", 211 | unit: unit, 212 | expression: { 213 | type: "CalendarExpression", 214 | direction: "up", 215 | unit: expr.unit, 216 | expression: expr 217 | } 218 | }; 219 | } 220 | / ord:OrdinalOffset _ "of" _ expr:CalendarExpression { 221 | return { 222 | type: "BinaryExpression", 223 | valueType: "moment", 224 | operator: "+", 225 | left: expr, 226 | right: ord 227 | }; 228 | } 229 | 230 | CalendarExpression 231 | = "today" { 232 | return { 233 | type: "TodayLiteral", 234 | valueType: "moment" 235 | }; 236 | } 237 | / "yesterday" { 238 | return { 239 | type: "YesterdayLiteral", 240 | valueType: "moment" 241 | }; 242 | } 243 | / "tomorrow" { 244 | return { 245 | type: "TomorrowLiteral", 246 | valueType: "moment" 247 | }; 248 | } 249 | / "this" _ unit:( CalendarUnit / DurationUnit ) { 250 | return { 251 | type: "CalendarExpression", 252 | valueType: "moment", 253 | direction: "down", 254 | unit: unit, 255 | expression: { 256 | type: "NowLiteral" 257 | } 258 | }; 259 | } 260 | / "last" _ unit:( CalendarUnit / DurationUnit ) { 261 | return { 262 | type: "CalendarExpression", 263 | valueType: "moment", 264 | direction: "down", 265 | unit: unit, 266 | expression: { 267 | type: "BinaryExpression", 268 | operator: "-", 269 | left: { 270 | type: "NowLiteral" 271 | }, 272 | right: { 273 | type: "MomentDuration", 274 | value: 1, 275 | unit: unit 276 | } 277 | } 278 | }; 279 | } 280 | / "next" _ unit:( CalendarUnit / DurationUnit ) { 281 | return { 282 | type: "CalendarExpression", 283 | valueType: "moment", 284 | direction: "down", 285 | unit: unit, 286 | expression: { 287 | type: "BinaryExpression", 288 | operator: "+", 289 | left: { 290 | type: "NowLiteral" 291 | }, 292 | right: { 293 | type: "MomentDuration", 294 | value: 1, 295 | unit: unit 296 | } 297 | } 298 | }; 299 | } 300 | / unit:CalendarUnit _ "of" _ expr:MomentExpression { 301 | return { 302 | type: "CalendarExpression", 303 | valueType: "moment", 304 | direction: "down", 305 | unit: unit, 306 | expression: expr 307 | } 308 | } 309 | / CalendarOffset 310 | 311 | OrdinalOffset 312 | = unit:CalendarUnit _ offset:Integer { 313 | return { 314 | type: "MomentDuration", 315 | valueType: "duration", 316 | value: offset - 1, 317 | unit: unit 318 | }; 319 | } 320 | 321 | OrdinalDuration 322 | = OrdinalOffset 323 | 324 | DurationLiteral 325 | = "forever" { 326 | return { 327 | type: "ForeverLiteral", 328 | valueType: "duration" 329 | }; 330 | } 331 | / TimeSpan 332 | / HumanDuration 333 | / ISODurationLiteral 334 | 335 | DurationExpression 336 | = left:DurationLiteral _ "and" _ right:DurationExpression { 337 | return { 338 | type: "BinaryExpression", 339 | valueType: "duration", 340 | operator: "+", 341 | left: left, 342 | right: right 343 | }; 344 | } 345 | / DurationLiteral 346 | 347 | MomentExpression 348 | = MomentString 349 | / "-" durationExpression:DurationExpression { 350 | return { 351 | type: "BinaryExpression", 352 | valueType: "moment", 353 | operator: "-", 354 | left: { 355 | type: "NowLiteral" 356 | }, 357 | right: durationExpression 358 | }; 359 | } 360 | / "+" durationExpression:DurationExpression { 361 | return { 362 | type: "BinaryExpression", 363 | valueType: "moment", 364 | operator: "+", 365 | left: { 366 | type: "NowLiteral" 367 | }, 368 | right: durationExpression 369 | }; 370 | } 371 | / durationExpression:DurationExpression _ "ago" { 372 | return { 373 | type: "BinaryExpression", 374 | valueType: "moment", 375 | operator: "-", 376 | left: { 377 | type: "NowLiteral" 378 | }, 379 | right: durationExpression 380 | }; 381 | } 382 | / durationExpression:DurationExpression _ "before" _ subExpression:( MomentExpression / CalendarExpression / MomentValue ) { 383 | return { 384 | type: "BinaryExpression", 385 | valueType: "moment", 386 | operator: "-", 387 | left: subExpression, 388 | right: durationExpression 389 | }; 390 | } 391 | / durationExpression:DurationExpression _ ("after" / "from") _ subExpression:( MomentExpression / CalendarExpression / MomentValue ) { 392 | return { 393 | type: "BinaryExpression", 394 | valueType: "moment", 395 | operator: "+", 396 | left: subExpression, 397 | right: durationExpression 398 | }; 399 | } 400 | 401 | MomentValue 402 | = CalendarExpression 403 | / MomentExpression 404 | / MomentDuration 405 | 406 | MomentDuration 407 | = OrdinalDuration 408 | / DurationExpression 409 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moment-parser", 3 | "version": "0.3.0", 4 | "description": "moment parser that supports natural language expressions", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "node_modules/.bin/istanbul cover -x 'lib/parser.js' node_modules/.bin/_mocha", 11 | "check-coverage": "node_modules/.bin/istanbul check-coverage", 12 | "postinstall": "node scripts/peg.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/juttle/moment-parser.git" 17 | }, 18 | "keywords": [ 19 | "moment", 20 | "parser" 21 | ], 22 | "author": "Will Welch ", 23 | "contributors": [ 24 | "Brian Kerezturi ", 25 | "Michael Demmer ", 26 | "Rodney Gomes " 27 | ], 28 | "license": "Apache-2.0", 29 | "bugs": { 30 | "url": "https://github.com/juttle/moment-parser/issues" 31 | }, 32 | "homepage": "https://github.com/juttle/moment-parser#readme", 33 | "dependencies": { 34 | "extendable-base": "^0.3.1", 35 | "moment": "^2.10.6", 36 | "pegjs": "^0.9.0" 37 | }, 38 | "devDependencies": { 39 | "chai": "^3.4.1", 40 | "istanbul": "^0.4.1", 41 | "mocha": "^2.3.3", 42 | "underscore": "^1.8.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts/peg.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Build the parser.js from the peg grammar. 3 | */ 4 | 5 | var path = require('path'); 6 | var PEG = require('pegjs'); 7 | var fs = require('fs'); 8 | 9 | var grammar = fs.readFileSync(path.join(__dirname, '../lib/parser.pegjs'), 'utf8'); 10 | 11 | var source = PEG.buildParser(grammar, { 12 | output: 'source', 13 | cache: true, 14 | exportVar: 'module.exports' 15 | }); 16 | 17 | var parser = "module.exports = " + source + ';\n'; 18 | 19 | fs.writeFileSync(path.join(__dirname, '../lib/parser.js'), parser, 'utf8'); 20 | -------------------------------------------------------------------------------- /test/api.spec.js: -------------------------------------------------------------------------------- 1 | var parser = require('..'); 2 | var _ = require('underscore'); 3 | var expect = require('chai').expect; 4 | var moment = require('moment'); 5 | 6 | var now = moment.utc('2015-01-01T01:02:03.456'); 7 | 8 | describe('moment-parser API ', function() { 9 | it('parses moment strings and returns moments', function() { 10 | expect(parser.parseMoment("now", {now: now}).isSame(now)).is.true; 11 | expect(parser.generateMoment(parser.parse("now"), {now: now}).isSame(now)).is.true; 12 | }); 13 | 14 | it('parses duration strings and returns durations', function() { 15 | var dur = moment.duration(3, 'weeks') 16 | expect(parser.parseDuration("3 weeks")).deep.equal(dur); 17 | expect(parser.generateDuration(parser.parse("3 weeks"))).deep.equal(dur); 18 | }); 19 | 20 | it('rejects durations when expecting a moment', function() { 21 | expect(function() { 22 | parser.parseMoment("3 weeks"); 23 | }).to.throw(parser.NotAMomentError); 24 | }); 25 | 26 | it('rejects moments when expecting a duration', function() { 27 | expect(function() { 28 | parser.parseDuration("now", {now: now}); 29 | }).to.throw(parser.NotADurationError); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/binary-expression-asts.spec.js: -------------------------------------------------------------------------------- 1 | var parser = require('..'); 2 | var _ = require('underscore'); 3 | var expect = require('chai').expect; 4 | var moment = require('moment'); 5 | 6 | describe('BinaryExpression parsing as AST', function() { 7 | var tests = { 8 | '+2h': { 9 | type: 'BinaryExpression', 10 | valueType: 'moment', 11 | operator: '+', 12 | left: { 13 | type: 'NowLiteral' 14 | }, 15 | right: { 16 | type: 'MomentDuration', 17 | valueType: 'duration', 18 | unit: "h", 19 | value: 2 20 | } 21 | }, 22 | 23 | '-12M': { 24 | type: 'BinaryExpression', 25 | operator: '-', 26 | valueType: 'moment', 27 | left: { 28 | type: 'NowLiteral' 29 | }, 30 | right: { 31 | type: 'MomentDuration', 32 | valueType: 'duration', 33 | unit: "M", 34 | value: 12 35 | } 36 | }, 37 | 38 | '2 minutes ago': { 39 | type: 'BinaryExpression', 40 | operator: '-', 41 | valueType: 'moment', 42 | left: { 43 | type: 'NowLiteral' 44 | }, 45 | right: { 46 | type: 'MomentDuration', 47 | valueType: 'duration', 48 | unit: "minute", 49 | value: 2 50 | } 51 | }, 52 | 53 | '2 minutes before 2015-01-01': { 54 | type: 'BinaryExpression', 55 | valueType: 'moment', 56 | operator: '-', 57 | left: { 58 | type: 'ISODateLiteral', 59 | valueType: 'moment', 60 | value: "2015-01-01T00:00:00" 61 | }, 62 | right: { 63 | type: 'MomentDuration', 64 | valueType: 'duration', 65 | unit: "minute", 66 | value: 2 67 | } 68 | }, 69 | 70 | '3 days after 2015-01-01': { 71 | type: 'BinaryExpression', 72 | valueType: 'moment', 73 | operator: '+', 74 | left: { 75 | type: 'ISODateLiteral', 76 | valueType: 'moment', 77 | value: "2015-01-01T00:00:00" 78 | }, 79 | right: { 80 | type: 'MomentDuration', 81 | valueType: 'duration', 82 | unit: "day", 83 | value: 3 84 | } 85 | }, 86 | 87 | '6 hours and 2 minutes and 30 seconds': { 88 | type: 'BinaryExpression', 89 | valueType: 'duration', 90 | operator: '+', 91 | left: { 92 | type: 'MomentDuration', 93 | valueType: 'duration', 94 | unit: "hour", 95 | value: 6 96 | }, 97 | right: { 98 | type: 'BinaryExpression', 99 | valueType: 'duration', 100 | operator: '+', 101 | left: { 102 | type: 'MomentDuration', 103 | valueType: 'duration', 104 | unit: "minute", 105 | value: 2 106 | }, 107 | right: { 108 | type: 'MomentDuration', 109 | valueType: 'duration', 110 | unit: "second", 111 | value: 30 112 | } 113 | } 114 | } 115 | }; 116 | 117 | _.each(tests, function(output, input) { 118 | it('parses "' + input + '"', function() { 119 | expect(parser.parse(input)).deep.equal(output); 120 | }); 121 | }); 122 | 123 | }); 124 | -------------------------------------------------------------------------------- /test/binary-expressions.spec.js: -------------------------------------------------------------------------------- 1 | var parser = require('..'); 2 | var _ = require('underscore'); 3 | var expect = require('chai').expect; 4 | var moment = require('moment'); 5 | 6 | var now = moment.utc('2015-01-01'); 7 | 8 | function is_same(a, b) { 9 | // compare a and b as moments or durations, return true if same 10 | if (a.isSame) { 11 | return a.isSame(b); // a moment 12 | } else { 13 | // duration. compare calendar and literal times separately 14 | return (a._milliseconds === b._milliseconds && 15 | a._days === b._days && 16 | a._months === b._months) 17 | } 18 | } 19 | 20 | describe('BinaryExpression parsing as moment', function() { 21 | var tests = { 22 | '+2h': now.clone().add(2, 'h'), 23 | '-12M': now.clone().subtract(12, 'M'), 24 | '2 minutes ago': now.clone().subtract(2, 'm'), 25 | '2 minutes before 2015-01-01': moment.utc('2015-01-01').subtract(2, 'm'), 26 | '3 days after 2015-01-01': moment.utc('2015-01-01').add(3, 'd') 27 | }; 28 | 29 | _.each(tests, function(expected, input) { 30 | it('handles "' + input + '"', function() { 31 | expect(parser.parse(input).valueType).equal('moment'); 32 | expect(is_same(expected, parser.parseMoment(input, {now: now}))).is.true; 33 | }); 34 | }); 35 | }); 36 | describe('BinaryExpression parsing as duration', function() { 37 | var tests = { 38 | '6 hours and 2 minutes and 30 seconds': moment.duration('06:02:30'), 39 | '2 years and 6 months and 20 days': moment.duration('20.00:00:00').add(30,'months') 40 | }; 41 | 42 | _.each(tests, function(expected, input) { 43 | it('handles "' + input + '"', function() { 44 | expect(parser.parse(input).valueType).equal('duration'); 45 | expect(is_same(expected, parser.parseDuration(input, {now: now}))).is.true; 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/calendar-expression-asts.spec.js: -------------------------------------------------------------------------------- 1 | var parser = require('..'); 2 | var _ = require('underscore'); 3 | var expect = require('chai').expect; 4 | var moment = require('moment'); 5 | 6 | describe('CalendarExpression parsing as AST', function() { 7 | var tests = { 8 | 'this year': { 9 | type: 'CalendarExpression', 10 | valueType: 'moment', 11 | direction: 'down', 12 | unit: 'year', 13 | expression: { 14 | type: 'NowLiteral' 15 | } 16 | }, 17 | 'month of 2015-01-01T01:02:03.456': { 18 | type: 'CalendarExpression', 19 | valueType: 'moment', 20 | direction: 'down', 21 | unit: 'month', 22 | expression: { 23 | type: 'ISODateLiteral', 24 | valueType: 'moment', 25 | value: '2015-01-01T01:02:03.456' 26 | } 27 | }, 28 | 'final month of this year': { 29 | type: 'CalendarExpression', 30 | valueType: 'moment', 31 | direction: 'down', 32 | unit: 'month', 33 | expression: { 34 | type: 'CalendarExpression', 35 | direction: 'up', 36 | unit: 'year', 37 | expression: { 38 | type: 'CalendarExpression', 39 | valueType: 'moment', 40 | direction: 'down', 41 | unit: 'year', 42 | expression: { 43 | type: 'NowLiteral' 44 | } 45 | } 46 | } 47 | }, 48 | 'final day of month of 2015-01-01T01:02:03.456': { 49 | type: 'CalendarExpression', 50 | valueType: 'moment', 51 | direction: 'down', 52 | unit: 'day', 53 | expression: { 54 | type: 'CalendarExpression', 55 | direction: 'up', 56 | unit: 'month', 57 | expression: { 58 | type: 'CalendarExpression', 59 | valueType: 'moment', 60 | direction: 'down', 61 | unit: 'month', 62 | expression: { 63 | type: 'ISODateLiteral', 64 | valueType: 'moment', 65 | value: '2015-01-01T01:02:03.456' 66 | } 67 | } 68 | } 69 | } 70 | }; 71 | 72 | _.each(tests, function(output, input) { 73 | it('parses "' + input + '"', function() { 74 | expect(parser.parse(input)).deep.equal(output); 75 | }); 76 | }); 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /test/calendar-expressions.spec.js: -------------------------------------------------------------------------------- 1 | var parser = require('..'); 2 | var _ = require('underscore'); 3 | var expect = require('chai').expect; 4 | var moment = require('moment'); 5 | 6 | var now = moment.utc('2015-01-01'); 7 | 8 | describe('CalendarExpression parsing as moment', function() { 9 | var tests = { 10 | 'this year': 11 | now.clone().startOf('year'), 12 | 'month of 2015-01-01T01:02:03.456': 13 | moment.utc('2015-01-01T01:02:03.456').startOf('month'), 14 | 'final month of this year': 15 | now.clone().endOf('year').startOf('month'), 16 | 'final day of month of 2015-01-01T01:02:03.456': 17 | moment.utc('2015-01-01T01:02:03.456').endOf('month').startOf('day') 18 | }; 19 | 20 | _.each(tests, function(expected, input) { 21 | it('handles "' + input + '"', function() { 22 | expect(parser.parse(input).valueType).equal('moment'); 23 | expect(parser.parseMoment(input, {now: now}).isSame(expected)).is.true; 24 | }); 25 | }); 26 | 27 | var throws = { 28 | 'last week of this year': parser.SyntaxError // last != final! 29 | }; 30 | 31 | _.each(throws, function(expected, input) { 32 | it('fails on "' + input + '"', function() { 33 | function parseInput() { 34 | parser.parseMoment(input); 35 | } 36 | expect(parseInput).to.throw(expected); 37 | }); 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /test/duration-expressions.spec.js: -------------------------------------------------------------------------------- 1 | var parser = require('..'); 2 | var _ = require('underscore'); 3 | var expect = require('chai').expect; 4 | var moment = require('moment'); 5 | 6 | var now = moment.utc('2015-01-01T01:02:03.456'); 7 | 8 | describe('Full duration grammar expression parsing', function() { 9 | var tests = { 10 | '1 year and 2 months': 11 | moment.duration({year: 1, month:2}), 12 | '1 year and 00:23:45': 13 | moment.duration({year: 1, minute:23, second:45}) 14 | }; 15 | 16 | _.each(tests, function(duration, input) { 17 | it('handles "' + input + '"', function() { 18 | expect(parser.parse(input).valueType).equal('duration'); 19 | expect(parser.parseDuration(input)).deep.equal(duration); 20 | }); 21 | }); 22 | 23 | var throws = { 24 | '1.5 months': parser.SyntaxError, 25 | '1.5 years': parser.SyntaxError 26 | }; 27 | 28 | _.each(throws, function(expected, input) { 29 | it('fails on "' + input + '"', function() { 30 | function parseInput() { 31 | parser.parseDuration(input); 32 | } 33 | expect(parseInput).to.throw(expected); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/duration-literal-asts.spec.js: -------------------------------------------------------------------------------- 1 | var parser = require('..'); 2 | var _ = require('underscore'); 3 | var expect = require('chai').expect; 4 | var moment = require('moment'); 5 | 6 | describe('literal duration parsing as AST', function() { 7 | var tests = { 8 | '1 hour': { 9 | type: 'MomentDuration', 10 | valueType: 'duration', 11 | unit: "hour", 12 | value: 1 13 | }, 14 | 15 | '100 s': { 16 | type: 'MomentDuration', 17 | valueType: 'duration', 18 | unit: "s", 19 | value: 100 20 | }, 21 | 22 | '1.5d': { 23 | type: 'MomentDuration', 24 | valueType: 'duration', 25 | unit: "d", 26 | value: 1.5 27 | }, 28 | 29 | '1m': { 30 | type: 'MomentDuration', 31 | valueType: 'duration', 32 | unit: "m", 33 | value: 1 34 | }, 35 | 36 | '12 M': { 37 | type: 'MomentDuration', 38 | valueType: 'duration', 39 | unit: "M", 40 | value: 12 41 | }, 42 | 43 | 'P1W': { 44 | type: 'ISODurationLiteral', 45 | valueType: 'duration', 46 | value: 'P1W' 47 | }, 48 | 49 | 'P1Y2M3DT4H5M6S': { 50 | type: 'ISODurationLiteral', 51 | valueType: 'duration', 52 | value: 'P1Y2M3DT4H5M6S' 53 | }, 54 | 55 | 'PT4H5M': { 56 | type: 'ISODurationLiteral', 57 | valueType: 'duration', 58 | value: 'PT4H5M' 59 | }, 60 | 61 | 'P1Y2M': { 62 | type: 'ISODurationLiteral', 63 | valueType: 'duration', 64 | value: 'P1Y2M' 65 | }, 66 | 67 | 'P2MT5M': { 68 | type: 'ISODurationLiteral', 69 | valueType: 'duration', 70 | value: 'P2MT5M' 71 | }, 72 | 73 | 'forever': { 74 | type: 'ForeverLiteral', 75 | valueType: 'duration' 76 | }, 77 | 78 | '01:23:45': { 79 | type: 'TimeSpan', 80 | valueType: 'duration', 81 | milliseconds: 0, 82 | seconds: 45, 83 | minutes: 23, 84 | hours: 1, 85 | days: 0, 86 | months: 0 87 | }, 88 | 89 | '1/23.01:23:45.067': { 90 | type: 'TimeSpan', 91 | valueType: 'duration', 92 | milliseconds: 67, 93 | seconds: 45, 94 | minutes: 23, 95 | hours: 1, 96 | days: 23, 97 | months: 1 98 | } 99 | 100 | }; 101 | 102 | _.each(tests, function(output, input) { 103 | it('parses "' + input + '"', function() { 104 | expect(parser.parse(input)).deep.equal(output); 105 | }); 106 | }); 107 | 108 | var throws = { 109 | '32 WHA': parser.SyntaxError 110 | }; 111 | 112 | _.each(throws, function(expected, input) { 113 | it('fails on "' + input + '"', function() { 114 | function parseInput() { 115 | parser.parseMoment(input); 116 | } 117 | expect(parseInput).to.throw(expected); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/duration-literals.spec.js: -------------------------------------------------------------------------------- 1 | var parser = require('..'); 2 | var _ = require('underscore'); 3 | var expect = require('chai').expect; 4 | var moment = require('moment'); 5 | 6 | describe('literal duration parsing as durations', function() { 7 | var tests = { 8 | '1 hour': moment.duration(1,'h'), 9 | '100 s': moment.duration(100,'s'), 10 | '1.5d': moment.duration(1.5,'d'), 11 | '1m': moment.duration(1,'m'), 12 | '12 M': moment.duration(12,'M'), 13 | 'P1W': moment.duration('P1W'), 14 | 'P1Y2M3DT4H5M6S': moment.duration('P1Y2M3DT4H5M6S'), 15 | 'PT4H5M': moment.duration('PT4H5M'), 16 | 'P1Y2M': moment.duration('P1Y2M'), 17 | 'P2MT5M': moment.duration('P2MT5M'), 18 | '00:00:00.123': moment.duration('00:00:00.123'), 19 | '01:23:45': moment.duration('01:23:45'), 20 | '01:23:45.678': moment.duration('01:23:45.678'), 21 | '23.01:23:45.067': moment.duration('23.01:23:45.067'), 22 | '1/23.01:23:45.067': moment.duration('23.01:23:45.067').add(1, 'M'), 23 | 'forever': moment.duration(Infinity) 24 | }; 25 | 26 | _.each(tests, function(duration, input) { 27 | it('handles "' + input + '"', function() { 28 | expect(parser.parse(input).valueType).equal('duration'); 29 | expect(parser.parseDuration(input)).deep.equal(duration); 30 | }); 31 | }); 32 | 33 | var throws = { 34 | '0:0:0': parser.SyntaxError, 35 | '000:00:00': parser.SyntaxError, 36 | '00:00:0.123': parser.SyntaxError, 37 | '1.5/23.01:23:45.678': parser.SyntaxError, 38 | '1.5/01:23:45.678': parser.SyntaxError, 39 | '1/23.01:23:45.0678':parser.SyntaxError 40 | }; 41 | 42 | _.each(throws, function(expected, input) { 43 | it('fails on "' + input + '"', function() { 44 | function parseInput() { 45 | parser.parseDuration(input); 46 | } 47 | expect(parseInput).to.throw(expected); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/moment-expressions.spec.js: -------------------------------------------------------------------------------- 1 | var parser = require('..'); 2 | var _ = require('underscore'); 3 | var expect = require('chai').expect; 4 | var moment = require('moment'); 5 | 6 | var now = moment.utc('2015-01-01T01:02:03.456'); 7 | 8 | describe('Full moment grammar expression parsing', function() { 9 | var tests = { 10 | 'day 5 of next month': 11 | now.clone().startOf('month').add(1, 'month').add(4, 'day'), 12 | 'month 2 of last year': 13 | now.clone().startOf('year').subtract(1, 'year').add(1, 'month'), 14 | '2 days after month of 2000-01-01T01:02:03.456': 15 | moment.utc('2000-01-01T01:02:03.456').startOf('month').add(2, 'day'), 16 | '-m': 17 | now.clone().subtract(1, 'minute'), 18 | '-M': 19 | now.clone().subtract(1, 'month'), 20 | 'last month': 21 | now.clone().startOf('month').subtract(1, 'month'), 22 | 'next year': 23 | now.clone().startOf('year').add(1, 'year'), 24 | '4 months after 2015-06-15': 25 | moment.utc('2015-06-15').add(4, 'month'), 26 | }; 27 | 28 | _.each(tests, function(expected, input) { 29 | it('handles "' + input + '"', function() { 30 | expect(parser.parse(input).valueType).equal('moment'); 31 | expect(parser.parseMoment(input, {now: now}).isSame(expected)).is.true; 32 | }); 33 | }); 34 | 35 | var throws = { 36 | 'last 3 months': parser.SyntaxError, 37 | 'next 2 days': parser.SyntaxError 38 | }; 39 | 40 | _.each(throws, function(expected, input) { 41 | it('fails on "' + input + '"', function() { 42 | function parseInput() { 43 | parser.parseMoment(input); 44 | } 45 | expect(parseInput).to.throw(expected); 46 | }); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /test/moment-literal-asts.spec.js: -------------------------------------------------------------------------------- 1 | var parser = require('..'); 2 | var _ = require('underscore'); 3 | var expect = require('chai').expect; 4 | var moment = require('moment'); 5 | 6 | var now = moment.utc('2015-01-01'); 7 | 8 | describe('literal moment parsing as AST', function() { 9 | var tests = { 10 | 'beginning': { 11 | type: 'BeginningLiteral', 12 | valueType: 'moment' 13 | }, 14 | 15 | '300': { 16 | type: 'UnixTimeLiteral', 17 | valueType: 'moment', 18 | value: 300 19 | }, 20 | 21 | '2015-01-01': { 22 | type: 'ISODateLiteral', 23 | valueType: 'moment', 24 | value: "2015-01-01T00:00:00" 25 | }, 26 | 27 | 'yesterday': { 28 | type: 'YesterdayLiteral', 29 | valueType: 'moment' 30 | }, 31 | 32 | 'today': { 33 | type: 'TodayLiteral', 34 | valueType: 'moment' 35 | }, 36 | 37 | 'now': { 38 | type: 'NowLiteral', 39 | valueType: 'moment' 40 | }, 41 | 42 | 'tomorrow': { 43 | type: 'TomorrowLiteral', 44 | valueType: 'moment' 45 | }, 46 | 47 | 'end': { 48 | type: 'EndLiteral', 49 | valueType: 'moment' 50 | } 51 | }; 52 | 53 | _.each(tests, function(expected, input) { 54 | it('handles "' + input + '"', function() { 55 | expect(parser.parse(input)).deep.equal(expected); 56 | }); 57 | }); 58 | 59 | var throws = { 60 | '2012/01/01': parser.SyntaxError 61 | }; 62 | 63 | _.each(throws, function(expected, input) { 64 | it('fails on "' + input + '"', function() { 65 | function parseInput() { 66 | parser.parseMoment(input); 67 | } 68 | expect(parseInput).to.throw(expected); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/moment-literals.spec.js: -------------------------------------------------------------------------------- 1 | var parser = require('..'); 2 | var _ = require('underscore'); 3 | var expect = require('chai').expect; 4 | var moment = require('moment'); 5 | 6 | var now = moment.utc('2015-01-01'); 7 | 8 | describe('literal moment parsing as moments', function() { 9 | var tests = { 10 | 'beginning': moment.utc(-100 * 1000000 * 1000 * 24 * 3600), 11 | 'end': moment.utc(100 * 1000000 * 1000 * 24 * 3600), 12 | '300': moment.utc(300 * 1000), 13 | '2015-01-01': moment.utc('2015-01-01'), 14 | 'yesterday': now.clone().startOf('day').subtract(1, 'day'), 15 | 'today': now.clone().startOf('day'), 16 | 'now': now, 17 | 'tomorrow': now.clone().startOf('day').add(1, 'day') 18 | }; 19 | 20 | _.each(tests, function(expected, input) { 21 | it('handles "' + input + '"', function() { 22 | expect(parser.parse(input).valueType).equal('moment'); 23 | var m = parser.parseMoment(input, {now: now}); 24 | expect(m.toISOString()).equal(expected.toISOString()); 25 | expect(m.isSame(expected)).is.true; 26 | }); 27 | }); 28 | 29 | it('allows the beginning / end value to be overridden', function() { 30 | var m; 31 | 32 | m = parser.parseMoment('beginning', {beginning: moment.utc(-Infinity)}); 33 | expect(m.valueOf().toString()).equals('NaN'); 34 | expect(m._i).equals(-Infinity); 35 | 36 | m = parser.parseMoment('beginning', {beginning: moment.utc(0)}); 37 | expect(m.toISOString()).equals('1970-01-01T00:00:00.000Z'); 38 | expect(m.valueOf().toString()).equals('0'); 39 | 40 | m = parser.parseMoment('end', {end: moment.utc('1234-05-06')}); 41 | expect(m.toISOString()).equals('1234-05-06T00:00:00.000Z'); 42 | 43 | m = parser.parseMoment('end', {end: moment.utc('6543-02-01')}); 44 | expect(m.toISOString()).equals('6543-02-01T00:00:00.000Z'); 45 | 46 | m = parser.parseMoment('end', {end: moment.utc(Infinity)}); 47 | expect(m.valueOf().toString()).equals('NaN'); 48 | expect(m._i).equals(Infinity); 49 | }); 50 | }); 51 | --------------------------------------------------------------------------------