├── .gitignore ├── .travis.yml ├── README.md ├── date-parser-browserify.coffee ├── date-parser.coffee ├── date-parser.js ├── lib ├── zh-TW.coffee └── zh-TW.js ├── package.json ├── script └── date-parser.min.js └── test ├── test-helpers.coffee └── zh-TW.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # Commenting this out is preferred by some people, see 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 28 | node_modules 29 | 30 | date-parser-browserify.js 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10.32" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # date-parser 2 | 3 | [![Build Status](https://travis-ci.org/Neson/date-parser.svg?branch=master)](https://travis-ci.org/Neson/date-parser) [![npm version](https://badge.fury.io/js/date-parser.svg)](http://badge.fury.io/js/date-parser) 4 | 5 | Parses spoken datetime sentence into an Date object. Only Traditional Chinese is supported now. 6 | 7 | ## Installation 8 | 9 | ### Node.js 10 | 11 | ```bash 12 | $ npm install --save date-parser 13 | $ node 14 | > dateParser = require('date-parser'); 15 | ``` 16 | 17 | ### Browser 18 | 19 | ```html 20 | 21 | ``` 22 | 23 | ## Usage 24 | 25 | ### I18n 26 | 27 | Select the default locale with: 28 | 29 | ```js 30 | dateParser.locale('zh-TW'); 31 | dateParser.timezone('Asia/Taipei'); 32 | ``` 33 | 34 | Or just specify it on the go: 35 | 36 | ```js 37 | dateParser.parse('二零一四年五月五日', 'Asia/Taipei', 'zh-TW'); 38 | ``` 39 | 40 | ### Datetime Parse 41 | 42 | The date-parser can parse any kind of spoken datetime into an Data object: 43 | 44 | ```js 45 | dateParser.parse('2014年10月20日'); // -> Mon Oct 20 2014 00:00:00 GMT+0800 (CST) 46 | dateParser.parse('二零一四年五月五日'); // -> Mon May 05 2014 00:00:00 GMT+0800 (CST) 47 | dateParser.parse('2010/2/28 晚上八點'); // -> Sun Feb 28 2010 20:00:00 GMT+0800 (CST) 48 | dateParser.parse('今天中午'); // -> (Today!) 12:00:00 GMT+0800 (CST) 49 | ``` 50 | 51 | You can just say an incomplete sentence, the parser will smartly guesses what you want: 52 | 53 | ```js 54 | dateParser.parse('中午'); // -> (Today!) 12:00:00 GMT+0800 (CST) 55 | dateParser.parse('今晚'); // -> (Today!) 18:00:00 GMT+0800 (CST) 56 | dateParser.parse('三點半'); // -> (Today!) 15:30:00 GMT+0800 (CST) (or 03:30:00, depends on current time) 57 | dateParser.parse('五月五'); // -> ... May 05 ... 00:00:00 GMT+0800 (CST) 58 | dateParser.parse('星期五晚上'); // -> Fri ... 18:00:00 GMT+0800 (CST) 59 | ``` 60 | 61 | Relative datetime is supported too. 62 | 63 | ```js 64 | dateParser.parse('下星期五晚上'); // -> Fri ... 18:00:00 GMT+0800 (CST) 65 | dateParser.parse('兩小時後'); // -> ... 66 | dateParser.parse('這週末'); // -> ... 67 | dateParser.parse('明天凌晨三點'); // -> (Tomorrow!) 03:00:00 GMT+0800 (CST) 68 | dateParser.parse('明年一月一號'); // -> ... Jan 01 ... 00:00:00 GMT+0800 (CST) 69 | ``` 70 | 71 | You can specify an ending datetime if needed: 72 | 73 | ```js 74 | meeting = dateParser.parse('早上八點到晚上十點'); // -> { (Today!) 08:00:00 GMT+0800 endTime: (Today!) 22:00:00 GMT+0800 } 75 | meeting.endTime; // -> (Today!) 22:00:00 GMT+0800 76 | party = dateParser.parse('週五 6:00pm ~ 週六 10:00am'); // -> { Fri ... 18:00:00 GMT+0800 endTime: Sat ... 08:00:00 GMT+0800 } 77 | party.endTime; // -> Sat ... 08:00:00 GMT+0800 78 | ``` 79 | 80 | BTW, the event's name and location can also be parsed out. 81 | 82 | ```js 83 | play = dateParser.parse('星期五晚上到下禮拜六早上要一直玩一直玩一直玩'); 84 | // -> { ... 18:00:00 GMT+0800 85 | // eventName: '一直玩一直玩一直玩', 86 | // endTime: ... 09:00:00 GMT+0800 } 87 | play.eventName; // -> '一直玩一直玩一直玩' 88 | dateParser.parse('今天晚上在聽風家有披薩吃'); 89 | // -> { ... 18:00:00 GMT+0800 90 | // location: '聽風家', 91 | // eventName: '披薩吃' } 92 | dateParser.parse('明天中午到下午要開會'); 93 | // -> { (Tomorrow!) 12:00:00 GMT+0800 94 | // eventName: '開會', 95 | // endTime: (Tomorrow!) 14:00:00 GMT+0800 } 96 | ``` 97 | -------------------------------------------------------------------------------- /date-parser-browserify.coffee: -------------------------------------------------------------------------------- 1 | if window 2 | window.dateParser = require('./date-parser') 3 | -------------------------------------------------------------------------------- /date-parser.coffee: -------------------------------------------------------------------------------- 1 | DateParser = 2 | 3 | LOCALES: 4 | 'zh-TW': require "./lib/zh-TW" 5 | 6 | DEFAULT_LOCALE: 'zh-TW' 7 | DEFAULT_TIMEZONE: 'Asia/Taipei' 8 | 9 | locale: (locale) -> 10 | if @LOCALES[locale] 11 | @DEFAULT_LOCALE = locale 12 | @LOCALES[locale].testStrings?.forEach (s) => 13 | @parse s 14 | else 15 | console.error "No such locale: #{locale}" 16 | 17 | timezone: (timezone) -> 18 | @DEFAULT_TIMEZONE = timezone 19 | 20 | parse: (text, timezone, locale) -> 21 | text = '' unless text 22 | locale = @DEFAULT_LOCALE unless locale 23 | timezone = @DEFAULT_TIMEZONE unless timezone 24 | expressions = @LOCALES[locale]?.expressions 25 | text = text.replace(RegExp(@LOCALES[locale]?.words?.interjections, 'g'), '') if @LOCALES[locale]?.words?.interjections 26 | if not expressions 27 | console.error "No such locale: #{locale}" 28 | return null 29 | for expression in @LOCALES[locale].expressions 30 | try 31 | result = expression(text, timezone) 32 | return result if result 33 | catch error 34 | console.error 'error', error 35 | return null 36 | 37 | number2integer: (text, locale) -> 38 | text = '' unless text 39 | locale = @DEFAULT_LOCALE unless locale 40 | return @LOCALES[locale]?.number2integer?(text) 41 | 42 | time2object: (text, locale) -> 43 | text = '' unless text 44 | locale = @DEFAULT_LOCALE unless locale 45 | return @LOCALES[locale]?.time2object?(text) 46 | 47 | dayTime2moment: (text, locale) -> 48 | text = '' unless text 49 | locale = @DEFAULT_LOCALE unless locale 50 | return @LOCALES[locale]?.dayTime2moment?(text) 51 | 52 | date2object: (text, locale) -> 53 | text = '' unless text 54 | locale = @DEFAULT_LOCALE unless locale 55 | return @LOCALES[locale]?.date2object?(text) 56 | 57 | dateExpression2moment: (text, locale) -> 58 | text = '' unless text 59 | locale = @DEFAULT_LOCALE unless locale 60 | return @LOCALES[locale]?.dateExpression2moment?(text) 61 | 62 | DateParser.locale('zh-TW') 63 | 64 | module.exports = DateParser 65 | -------------------------------------------------------------------------------- /date-parser.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.8.0 2 | (function() { 3 | var DateParser; 4 | 5 | DateParser = { 6 | LOCALES: { 7 | 'zh-TW': require("./lib/zh-TW") 8 | }, 9 | DEFAULT_LOCALE: 'zh-TW', 10 | DEFAULT_TIMEZONE: 'Asia/Taipei', 11 | locale: function(locale) { 12 | var _ref; 13 | if (this.LOCALES[locale]) { 14 | this.DEFAULT_LOCALE = locale; 15 | return (_ref = this.LOCALES[locale].testStrings) != null ? _ref.forEach((function(_this) { 16 | return function(s) { 17 | return _this.parse(s); 18 | }; 19 | })(this)) : void 0; 20 | } else { 21 | return console.error("No such locale: " + locale); 22 | } 23 | }, 24 | timezone: function(timezone) { 25 | return this.DEFAULT_TIMEZONE = timezone; 26 | }, 27 | parse: function(text, timezone, locale) { 28 | var error, expression, expressions, result, _i, _len, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; 29 | if (!text) { 30 | text = ''; 31 | } 32 | if (!locale) { 33 | locale = this.DEFAULT_LOCALE; 34 | } 35 | if (!timezone) { 36 | timezone = this.DEFAULT_TIMEZONE; 37 | } 38 | expressions = (_ref = this.LOCALES[locale]) != null ? _ref.expressions : void 0; 39 | if ((_ref1 = this.LOCALES[locale]) != null ? (_ref2 = _ref1.words) != null ? _ref2.interjections : void 0 : void 0) { 40 | text = text.replace(RegExp((_ref3 = this.LOCALES[locale]) != null ? (_ref4 = _ref3.words) != null ? _ref4.interjections : void 0 : void 0, 'g'), ''); 41 | } 42 | if (!expressions) { 43 | console.error("No such locale: " + locale); 44 | return null; 45 | } 46 | _ref5 = this.LOCALES[locale].expressions; 47 | for (_i = 0, _len = _ref5.length; _i < _len; _i++) { 48 | expression = _ref5[_i]; 49 | try { 50 | result = expression(text, timezone); 51 | if (result) { 52 | return result; 53 | } 54 | } catch (_error) { 55 | error = _error; 56 | console.error('error', error); 57 | } 58 | } 59 | return null; 60 | }, 61 | number2integer: function(text, locale) { 62 | var _ref; 63 | if (!text) { 64 | text = ''; 65 | } 66 | if (!locale) { 67 | locale = this.DEFAULT_LOCALE; 68 | } 69 | return (_ref = this.LOCALES[locale]) != null ? typeof _ref.number2integer === "function" ? _ref.number2integer(text) : void 0 : void 0; 70 | }, 71 | time2object: function(text, locale) { 72 | var _ref; 73 | if (!text) { 74 | text = ''; 75 | } 76 | if (!locale) { 77 | locale = this.DEFAULT_LOCALE; 78 | } 79 | return (_ref = this.LOCALES[locale]) != null ? typeof _ref.time2object === "function" ? _ref.time2object(text) : void 0 : void 0; 80 | }, 81 | dayTime2moment: function(text, locale) { 82 | var _ref; 83 | if (!text) { 84 | text = ''; 85 | } 86 | if (!locale) { 87 | locale = this.DEFAULT_LOCALE; 88 | } 89 | return (_ref = this.LOCALES[locale]) != null ? typeof _ref.dayTime2moment === "function" ? _ref.dayTime2moment(text) : void 0 : void 0; 90 | }, 91 | date2object: function(text, locale) { 92 | var _ref; 93 | if (!text) { 94 | text = ''; 95 | } 96 | if (!locale) { 97 | locale = this.DEFAULT_LOCALE; 98 | } 99 | return (_ref = this.LOCALES[locale]) != null ? typeof _ref.date2object === "function" ? _ref.date2object(text) : void 0 : void 0; 100 | }, 101 | dateExpression2moment: function(text, locale) { 102 | var _ref; 103 | if (!text) { 104 | text = ''; 105 | } 106 | if (!locale) { 107 | locale = this.DEFAULT_LOCALE; 108 | } 109 | return (_ref = this.LOCALES[locale]) != null ? typeof _ref.dateExpression2moment === "function" ? _ref.dateExpression2moment(text) : void 0 : void 0; 110 | } 111 | }; 112 | 113 | DateParser.locale('zh-TW'); 114 | 115 | module.exports = DateParser; 116 | 117 | }).call(this); 118 | -------------------------------------------------------------------------------- /lib/zh-TW.coffee: -------------------------------------------------------------------------------- 1 | Moment = require('moment-timezone') 2 | 3 | words = 4 | at: '(?:在|於|@|要在)' 5 | zero: '(?:零|0|0)' 6 | one: '(?:一|1|1)' 7 | two: '(?:二|兩|2|2)' 8 | three: '(?:三|3|3)' 9 | four: '(?:四|4|4)' 10 | five: '(?:五|5|5)' 11 | six: '(?:六|6|6)' 12 | seven: '(?:七|7|7)' 13 | sun: '(?:日|天)' 14 | eight: '(?:八|8|8)' 15 | nine: '(?:九|9|9)' 16 | ten: '十' 17 | half: '半' 18 | end: '末' 19 | dot: '(?:\\.|點|:|:)' 20 | hour: '(?:時|小時|點|:|:)' 21 | minute: '(?:分|分鐘|:|:)' 22 | second: '秒' 23 | morning: '(?:早|早上|早晨|am|AM)' 24 | noon: '(?:中午)' 25 | afternoon: '(?:下午|午後|pm|PM)' 26 | night: '(?:晚|晚上|晚間|夜晚|夜間)' 27 | midnight: '(?:午夜|半夜|凌晨)' 28 | after: '後' 29 | today: '(?:今|今天|今日)' 30 | tomorrow: '(?:明|明天|明日)' 31 | acquired: '(?:後天|後日)' 32 | yesterday: '(?:昨天|昨日)' 33 | the_day_before_yesterday: '(?:前天|前日)' 34 | this: '(?:這|這個|今)' 35 | next: '(?:下|下個|明)' 36 | previous: '(?:上|上個|去|昨)' 37 | week: '(?:禮拜|星期|週|周)' 38 | year: '(?:年|\\/|\\-)' 39 | year_relative: '(?:今年|去年|明年)' 40 | month: '(?:月|\\/|\\-)' 41 | month_relative: '(?:這個月|上個月|下個月)' 42 | day: '(?:日|號|\\/|\\-)' 43 | to: '(?:到|-|-|~)' 44 | event_prefix: '(?:有|要|的|,| )' 45 | event_postfix: '(?:在|,| )' 46 | separator: '(?:個|的|,| )' 47 | unit: '(?:個)' 48 | interjections: '(?:左右|大概|又|吧)' 49 | 50 | words.numbers = "(?:#{words.zero}|#{words.one}|#{words.two}|#{words.three}|#{words.four}|#{words.five}|#{words.six}|#{words.seven}|#{words.eight}|#{words.nine}|#{words.ten}|#{words.half}|#{words.unit})" 51 | words.zero_to_nine = "(?:#{words.zero}|#{words.one}|#{words.two}|#{words.three}|#{words.four}|#{words.five}|#{words.six}|#{words.seven}|#{words.eight}|#{words.nine})" 52 | words.am = "(?:#{words.midnight}|#{words.morning})" 53 | words.pm = "(?:#{words.noon}|#{words.afternoon}|#{words.night})" 54 | words.this_previous_next = "(?:#{words.this}|#{words.previous}|#{words.next})" 55 | words.dayPeriods = "(?:#{words.morning}|#{words.noon}|#{words.afternoon}|#{words.night}|#{words.midnight})" 56 | words.time = "(?:#{words.numbers}+#{words.hour}(?:#{words.numbers}+)?(?:#{words.minute})?(?:#{words.numbers}+)?(?:#{words.second})?)" 57 | words.like_time = "(?:#{words.numbers}+(?:#{words.numbers}|#{words.hour}|#{words.minute}|#{words.second}){1,12})" 58 | words.dayTime = "(?:(?:#{words.dayPeriods}?#{words.separator}?#{words.like_time}) ?#{words.dayPeriods}?|#{words.dayPeriods})" 59 | words.year_month_day = "(?:#{words.year}|#{words.month}|#{words.day}|#{words.year_relative}|#{words.month_relative})" 60 | words.date = "(?:(?:(?:(?:#{words.numbers}{1,4}|#{words.this_previous_next}) ?#{words.year})? ?(?:#{words.numbers}{1,3}|#{words.this_previous_next}) ?#{words.month})? ?#{words.numbers}{1,3} ?#{words.day}?)" 61 | words.like_date = "(?:(?:#{words.numbers}|#{words.year_month_day}){1,12}#{words.year_month_day}#{words.numbers}{0,3})" 62 | words.weekdays = "(?:#{words.zero}|#{words.one}|#{words.two}|#{words.three}|#{words.four}|#{words.five}|#{words.six}|#{words.seven}|#{words.sun}|#{words.end})" 63 | words.weekExpression = "(?:#{words.this_previous_next}?#{words.week}#{words.weekdays})" 64 | words.dateExpression = "(?:#{words.like_date}|#{words.weekExpression}|#{words.today}|#{words.tomorrow}|#{words.acquired}|#{words.yesterday}|#{words.the_day_before_yesterday})" 65 | words.separators = "(?:#{words.event_prefix}|#{words.event_postfix})" 66 | 67 | number2integer = (number) -> 68 | return null if not number 69 | if match = number.match RegExp("(#{words.zero_to_nine})(#{words.zero_to_nine})(#{words.zero_to_nine})(#{words.zero_to_nine})") 70 | return number2integer(match[1])*1000 + number2integer(match[2])*100 + number2integer(match[3])*10 + number2integer(match[4]) 71 | else if match = number.match RegExp("(#{words.zero_to_nine})(#{words.ten})?(#{words.zero_to_nine})#{words.unit}?") 72 | n = number2integer(match[3]) 73 | n = 0 if n >= 10 74 | return number2integer(match[1])*10 + n 75 | else if match = number.match RegExp("#{words.ten}(#{words.zero_to_nine})#{words.unit}?") 76 | return number2integer(match[1]) + 10 77 | else if match = number.match RegExp("(#{words.zero_to_nine})#{words.ten}#{words.unit}?") 78 | return number2integer(match[1])*10 79 | else if match = number.match RegExp("(#{words.numbers}+)#{words.unit}#{words.half}") 80 | return number2integer(match[1]) + 0.5 81 | else if number.match RegExp(words.half) 82 | return 0.5 83 | else if number.match RegExp(words.zero) 84 | return 0 85 | else if number.match RegExp(words.one) 86 | return 1 87 | else if number.match RegExp(words.two) 88 | return 2 89 | else if number.match RegExp(words.three) 90 | return 3 91 | else if number.match RegExp(words.four) 92 | return 4 93 | else if number.match RegExp(words.five) 94 | return 5 95 | else if number.match RegExp(words.six) 96 | return 6 97 | else if number.match RegExp(words.seven) 98 | return 7 99 | else if number.match RegExp(words.sun) 100 | return 7 101 | else if number.match RegExp(words.eight) 102 | return 8 103 | else if number.match RegExp(words.nine) 104 | return 9 105 | else if number.match RegExp(words.ten) 106 | return 10 107 | else 108 | return null 109 | 110 | time2object = (time) -> 111 | return null if not time 112 | if match = time.match RegExp("(?:(#{words.numbers}+)#{words.hour}(#{words.numbers}*)(?:#{words.minute})?(#{words.numbers}*)(?:#{words.second})?)") 113 | hour = number2integer(match[1]) or 0 114 | minute = number2integer(match[2]) or 0 115 | second = number2integer(match[3]) or 0 116 | minute = 30 if minute == 0.5 117 | if hour == 30 118 | hour = 0 119 | minute += 30 120 | if (f = hour % 1) > 0 121 | hour -= f 122 | minute += 30 123 | while second > 60 124 | second -= 60 125 | minute += 1 126 | while minute > 60 127 | minute -= 60 128 | hour += 1 129 | return { 130 | hour: hour, 131 | minute: minute, 132 | second: second 133 | } 134 | else 135 | return null 136 | 137 | dayTime2moment = (daytime, timezone, moment) -> 138 | return null if not daytime 139 | if match = daytime.match RegExp("(?:(?:(#{words.dayPeriods})?#{words.separator}?(#{words.time}) ?(#{words.dayPeriods})?)|(#{words.dayPeriods}))") 140 | if period = match[4] 141 | moment = moment or Moment().tz(timezone) 142 | moment.endMoment = moment?.endMoment or (moment and Moment(moment).tz(timezone)) or Moment().tz(timezone) 143 | moment.minute(0) 144 | moment.second(0) 145 | moment.endMoment.minute(0) 146 | moment.endMoment.second(0) 147 | if period.match RegExp(words.morning) 148 | moment.hour(9) 149 | moment.endMoment.hour(11) 150 | else if period.match RegExp(words.noon) 151 | moment.hour(12) 152 | moment.endMoment.hour(13) 153 | else if period.match RegExp(words.afternoon) 154 | moment.hour(14) 155 | moment.endMoment.hour(16) 156 | else if period.match RegExp(words.night) 157 | moment.hour(18) 158 | moment.endMoment.hour(20) 159 | else if period.match RegExp(words.midnight) 160 | moment.hour(0) 161 | moment.endMoment.hour(4) 162 | return moment 163 | else 164 | period = match[1] or match[3] or '' 165 | time = time2object(match[2]) 166 | return null if not time 167 | time.hour += 12 if period.match(RegExp(words.pm)) and time.hour < 12 168 | moment = moment or Moment().tz(timezone) 169 | if time.hour < 12 and not period 170 | time.hour += 12 if time.hour > (moment.hour() - 12) and time.hour < moment.hour() 171 | moment.hour(time.hour) 172 | moment.minute(time.minute or 0) 173 | moment.second(time.second or 0) 174 | return moment 175 | else 176 | return null 177 | 178 | date2object = (date) -> 179 | return null if not date 180 | if match = date.match RegExp("(?:(?:(?:(#{words.numbers}+|#{words.this_previous_next}) ?#{words.year})? ?(#{words.numbers}+|#{words.separator}|#{words.this_previous_next}) ?#{words.month})? ?(?:(#{words.numbers}+) ?#{words.day}?))") 181 | now = Moment() 182 | year = number2integer(match[1]) or null 183 | if year and year < 100 184 | c = parseInt(now.year()/100)*100 185 | year = year + c 186 | month = number2integer(match[2]) or null 187 | day = number2integer(match[3]) or null 188 | if match[1]?.match RegExp(words.this) 189 | year = now.year() 190 | else if match[1]?.match RegExp(words.previous) 191 | year = now.year() - 1 192 | else if match[1]?.match RegExp(words.next) 193 | year = now.year() + 1 194 | if match[2]?.match RegExp(words.this) 195 | year = now.month() + 1 196 | else if match[2]?.match RegExp(words.previous) 197 | month = now.month() 198 | else if match[2]?.match RegExp(words.next) 199 | month = now.month() + 2 200 | return { 201 | year: year, 202 | month: month, 203 | day: day 204 | } 205 | else 206 | return null 207 | 208 | dateExpression2moment = (dateExp, timezone) -> 209 | return null if not dateExp 210 | moment = Moment().tz(timezone) 211 | moment.hour(0) 212 | moment.minute(0) 213 | moment.second(0) 214 | if match = dateExp.match RegExp("#{words.today}[^#{words.year}]*$") 215 | return moment 216 | else if match = dateExp.match RegExp("#{words.tomorrow}[^#{words.year}]*$") 217 | moment.date moment.date() + 1 218 | return moment 219 | else if match = dateExp.match RegExp(words.acquired) 220 | moment.date moment.date() + 2 221 | return moment 222 | else if match = dateExp.match RegExp(words.yesterday) 223 | moment.date moment.date() - 1 224 | return moment 225 | else if match = dateExp.match RegExp(words.the_day_before_yesterday) 226 | moment.date moment.date() - 2 227 | return moment 228 | else if match = dateExp.match RegExp("(#{words.this_previous_next})?#{words.week}(#{words.weekdays})") 229 | if match[1]?.match RegExp(words.previous) 230 | moment.date moment.date() - 7 231 | else if match[1]?.match RegExp(words.next) 232 | moment.date moment.date() + 7 233 | day = number2integer(match[2]) 234 | if match[2].match RegExp(words.end) 235 | day = 6 236 | day = 0 if day >= 7 237 | moment.day day 238 | if match[2].match RegExp(words.end) 239 | moment.endMoment = Moment(moment).tz(timezone) 240 | moment.endMoment.date moment.date() + 1 241 | return moment 242 | else if match = dateExp.match RegExp(words.like_date) 243 | dateObj = date2object(match[0]) 244 | return null unless dateObj 245 | moment.date(dateObj.day) if dateObj.day 246 | moment.month(dateObj.month - 1) if dateObj.month 247 | moment.year(dateObj.year) if dateObj.year 248 | return moment 249 | else return null 250 | 251 | expressions = [] 252 | 253 | # 「<事件> 時間後 <在...> <事件>」 254 | expressions.push (text, timezone) -> 255 | if match = text.match RegExp("^(?:([^#{words.separators}]+) ?#{words.event_postfix} ?)?(#{words.time})#{words.after} ?(?:#{words.at} ?([^#{words.separators}]+))? ?(?:#{words.event_prefix}#{words.separator}?([^#{words.separators}]+))?$") 256 | time = time2object(match[2]) 257 | location = match[3] 258 | eventName = match[1] or match[4] 259 | moment = Moment().tz(timezone) 260 | moment.hour moment.hour() + time.hour 261 | moment.minute moment.minute() + time.minute 262 | moment.second moment.second() + time.second 263 | date = moment.toDate() 264 | date.location = location if location 265 | date.eventName = eventName if eventName 266 | return date 267 | 268 | # 「<事件> 日期 <時間> 到 日期 <時間> <在...> <事件>」 269 | expressions.push (text, timezone) -> 270 | if match = text.match RegExp("^(?:([^#{words.separators}]+) ?#{words.event_postfix})? ?(#{words.dateExpression}) ?(#{words.dayTime})? ?(?:#{words.to} ?(#{words.dateExpression}) ?(#{words.dayTime})? ?) ?(?:#{words.at} ?([^#{words.separators}]+))? ?(?:#{words.event_prefix}#{words.separator}?([^#{words.separators}]+))?$") 271 | moment = dateExpression2moment(match[2], timezone) or Moment().tz(timezone) 272 | moment = dayTime2moment(match[3], timezone, moment) if match[3] 273 | if not match[3] and not match[5] 274 | fullDay = true 275 | location = match[6] 276 | eventName = match[1] or match[7] 277 | moment.endMoment = dateExpression2moment(match[4], timezone) if match[4] 278 | if moment.endMoment and match[5] 279 | moment.endMoment = dayTime2moment(match[5], timezone, moment.endMoment) 280 | else if moment.endMoment and not match[5] 281 | moment.endMoment.hour(23) 282 | moment.endMoment.minute(59) 283 | moment.endMoment.second(59) 284 | date = moment.toDate() 285 | date.endTime = moment.endMoment.toDate() if moment.endMoment 286 | date.location = location if location 287 | date.eventName = eventName if eventName 288 | date.fullDay = fullDay if fullDay 289 | return date 290 | 291 | # 「<事件> 日期 時間 <到 時間> <在...> <事件>」 292 | expressions.push (text, timezone) -> 293 | if match = text.match RegExp("^(?:([^#{words.separators}]+) ?#{words.event_postfix})? ?(?:(#{words.dateExpression})) ?(#{words.dayTime}) ?(?:#{words.to} ?(#{words.dayTime}) ?)? ?(?:#{words.at} ?([^#{words.separators}]+))? ?(?:#{words.event_prefix}#{words.separator}?([^#{words.separators}]+))?$") 294 | moment = dateExpression2moment(match[2], timezone) or Moment().tz(timezone) 295 | moment = dayTime2moment(match[3], timezone, moment) if match[3] 296 | moment.endMoment = dayTime2moment(match[4], timezone, Moment(moment)) if match[4] 297 | location = match[5] 298 | eventName = match[1] or match[6] 299 | date = moment.toDate() 300 | date.location = location if location 301 | date.eventName = eventName if eventName 302 | date.endTime = moment.endMoment.toDate() if moment.endMoment 303 | return date 304 | 305 | # 「<事件> 時間 <到 時間> <在...> <事件>」 306 | expressions.push (text, timezone) -> 307 | if match = text.match RegExp("^(?:([^#{words.separators}]+) ?#{words.event_postfix})? ?(#{words.dayTime}) ?(?:#{words.to} ?(#{words.dayTime}) ?)? ?(?:#{words.at} ?([^#{words.separators}]+))? ?(?:#{words.event_prefix}#{words.separator}?([^#{words.separators}]+))?$") 308 | moment = dayTime2moment(match[2], timezone) if match[2] 309 | moment.endMoment = dayTime2moment(match[3], timezone) 310 | location = match[4] 311 | eventName = match[1] or match[5] 312 | date = moment.toDate() 313 | date.location = location if location 314 | date.eventName = eventName if eventName 315 | date.endTime = moment.endMoment.toDate() if moment.endMoment 316 | return date 317 | 318 | # 「<事件> 日期 <時間> <到 日期 <時間>> <在...> <事件>」 319 | expressions.push (text, timezone) -> 320 | if match = text.match RegExp("^(?:([^#{words.separators}]+) ?#{words.event_postfix})? ?(#{words.dateExpression}) ?(#{words.dayTime})? ?(?:#{words.to} ? (#{words.dateExpression}) ?(#{words.dayTime})? ?)? ?(?:#{words.at} ?([^#{words.separators}]+))? ?(?:#{words.event_prefix}#{words.separator}?([^#{words.separators}]+))?$") 321 | location = match[6] 322 | eventName = match[1] or match[7] 323 | moment = dateExpression2moment(match[2], timezone) or Moment().tz(timezone) 324 | moment = dayTime2moment(match[3], timezone, moment) if match[3] 325 | if not match[3] and not match[5] 326 | fullDay = true 327 | moment.endMoment = dateExpression2moment(match[5], timezone, moment.endMoment) if match[5] 328 | if moment.endMoment and match[5] 329 | moment.endMoment = dayTime2moment(match[5], timezone, moment.endMoment) 330 | else if moment.endMoment and not match[5] 331 | moment.endMoment.hour(23) 332 | moment.endMoment.minute(59) 333 | moment.endMoment.second(59) 334 | date = moment.toDate() 335 | date.location = location if location 336 | date.eventName = eventName if eventName 337 | date.endTime = moment.endMoment.toDate() if moment.endMoment 338 | date.fullDay = fullDay if fullDay 339 | return date 340 | 341 | module.exports = 342 | words: words 343 | expressions: expressions 344 | number2integer: number2integer 345 | time2object: time2object 346 | dayTime2moment: dayTime2moment 347 | date2object: date2object 348 | dateExpression2moment: dateExpression2moment 349 | testStrings: ['明天晚上到下禮拜六早上要開會', '10月10號 早上 10:00 ~ 下午 4:00', '十月十號'] 350 | -------------------------------------------------------------------------------- /lib/zh-TW.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.8.0 2 | (function() { 3 | var Moment, date2object, dateExpression2moment, dayTime2moment, expressions, number2integer, time2object, words; 4 | 5 | Moment = require('moment-timezone'); 6 | 7 | words = { 8 | at: '(?:在|於|@|要在)', 9 | zero: '(?:零|0|0)', 10 | one: '(?:一|1|1)', 11 | two: '(?:二|兩|2|2)', 12 | three: '(?:三|3|3)', 13 | four: '(?:四|4|4)', 14 | five: '(?:五|5|5)', 15 | six: '(?:六|6|6)', 16 | seven: '(?:七|7|7)', 17 | sun: '(?:日|天)', 18 | eight: '(?:八|8|8)', 19 | nine: '(?:九|9|9)', 20 | ten: '十', 21 | half: '半', 22 | end: '末', 23 | dot: '(?:\\.|點|:|:)', 24 | hour: '(?:時|小時|點|:|:)', 25 | minute: '(?:分|分鐘|:|:)', 26 | second: '秒', 27 | morning: '(?:早|早上|早晨|am|AM)', 28 | noon: '(?:中午)', 29 | afternoon: '(?:下午|午後|pm|PM)', 30 | night: '(?:晚|晚上|晚間|夜晚|夜間)', 31 | midnight: '(?:午夜|半夜|凌晨)', 32 | after: '後', 33 | today: '(?:今|今天|今日)', 34 | tomorrow: '(?:明|明天|明日)', 35 | acquired: '(?:後天|後日)', 36 | yesterday: '(?:昨天|昨日)', 37 | the_day_before_yesterday: '(?:前天|前日)', 38 | "this": '(?:這|這個|今)', 39 | next: '(?:下|下個|明)', 40 | previous: '(?:上|上個|去|昨)', 41 | week: '(?:禮拜|星期|週|周)', 42 | year: '(?:年|\\/|\\-)', 43 | year_relative: '(?:今年|去年|明年)', 44 | month: '(?:月|\\/|\\-)', 45 | month_relative: '(?:這個月|上個月|下個月)', 46 | day: '(?:日|號|\\/|\\-)', 47 | to: '(?:到|-|-|~)', 48 | event_prefix: '(?:有|要|的|,| )', 49 | event_postfix: '(?:在|,| )', 50 | separator: '(?:個|的|,| )', 51 | unit: '(?:個)', 52 | interjections: '(?:左右|大概|又|吧)' 53 | }; 54 | 55 | words.numbers = "(?:" + words.zero + "|" + words.one + "|" + words.two + "|" + words.three + "|" + words.four + "|" + words.five + "|" + words.six + "|" + words.seven + "|" + words.eight + "|" + words.nine + "|" + words.ten + "|" + words.half + "|" + words.unit + ")"; 56 | 57 | words.zero_to_nine = "(?:" + words.zero + "|" + words.one + "|" + words.two + "|" + words.three + "|" + words.four + "|" + words.five + "|" + words.six + "|" + words.seven + "|" + words.eight + "|" + words.nine + ")"; 58 | 59 | words.am = "(?:" + words.midnight + "|" + words.morning + ")"; 60 | 61 | words.pm = "(?:" + words.noon + "|" + words.afternoon + "|" + words.night + ")"; 62 | 63 | words.this_previous_next = "(?:" + words["this"] + "|" + words.previous + "|" + words.next + ")"; 64 | 65 | words.dayPeriods = "(?:" + words.morning + "|" + words.noon + "|" + words.afternoon + "|" + words.night + "|" + words.midnight + ")"; 66 | 67 | words.time = "(?:" + words.numbers + "+" + words.hour + "(?:" + words.numbers + "+)?(?:" + words.minute + ")?(?:" + words.numbers + "+)?(?:" + words.second + ")?)"; 68 | 69 | words.like_time = "(?:" + words.numbers + "+(?:" + words.numbers + "|" + words.hour + "|" + words.minute + "|" + words.second + "){1,12})"; 70 | 71 | words.dayTime = "(?:(?:" + words.dayPeriods + "?" + words.separator + "?" + words.like_time + ") ?" + words.dayPeriods + "?|" + words.dayPeriods + ")"; 72 | 73 | words.year_month_day = "(?:" + words.year + "|" + words.month + "|" + words.day + "|" + words.year_relative + "|" + words.month_relative + ")"; 74 | 75 | words.date = "(?:(?:(?:(?:" + words.numbers + "{1,4}|" + words.this_previous_next + ") ?" + words.year + ")? ?(?:" + words.numbers + "{1,3}|" + words.this_previous_next + ") ?" + words.month + ")? ?" + words.numbers + "{1,3} ?" + words.day + "?)"; 76 | 77 | words.like_date = "(?:(?:" + words.numbers + "|" + words.year_month_day + "){1,12}" + words.year_month_day + words.numbers + "{0,3})"; 78 | 79 | words.weekdays = "(?:" + words.zero + "|" + words.one + "|" + words.two + "|" + words.three + "|" + words.four + "|" + words.five + "|" + words.six + "|" + words.seven + "|" + words.sun + "|" + words.end + ")"; 80 | 81 | words.weekExpression = "(?:" + words.this_previous_next + "?" + words.week + words.weekdays + ")"; 82 | 83 | words.dateExpression = "(?:" + words.like_date + "|" + words.weekExpression + "|" + words.today + "|" + words.tomorrow + "|" + words.acquired + "|" + words.yesterday + "|" + words.the_day_before_yesterday + ")"; 84 | 85 | words.separators = "(?:" + words.event_prefix + "|" + words.event_postfix + ")"; 86 | 87 | number2integer = function(number) { 88 | var match, n; 89 | if (!number) { 90 | return null; 91 | } 92 | if (match = number.match(RegExp("(" + words.zero_to_nine + ")(" + words.zero_to_nine + ")(" + words.zero_to_nine + ")(" + words.zero_to_nine + ")"))) { 93 | return number2integer(match[1]) * 1000 + number2integer(match[2]) * 100 + number2integer(match[3]) * 10 + number2integer(match[4]); 94 | } else if (match = number.match(RegExp("(" + words.zero_to_nine + ")(" + words.ten + ")?(" + words.zero_to_nine + ")" + words.unit + "?"))) { 95 | n = number2integer(match[3]); 96 | if (n >= 10) { 97 | n = 0; 98 | } 99 | return number2integer(match[1]) * 10 + n; 100 | } else if (match = number.match(RegExp("" + words.ten + "(" + words.zero_to_nine + ")" + words.unit + "?"))) { 101 | return number2integer(match[1]) + 10; 102 | } else if (match = number.match(RegExp("(" + words.zero_to_nine + ")" + words.ten + words.unit + "?"))) { 103 | return number2integer(match[1]) * 10; 104 | } else if (match = number.match(RegExp("(" + words.numbers + "+)" + words.unit + words.half))) { 105 | return number2integer(match[1]) + 0.5; 106 | } else if (number.match(RegExp(words.half))) { 107 | return 0.5; 108 | } else if (number.match(RegExp(words.zero))) { 109 | return 0; 110 | } else if (number.match(RegExp(words.one))) { 111 | return 1; 112 | } else if (number.match(RegExp(words.two))) { 113 | return 2; 114 | } else if (number.match(RegExp(words.three))) { 115 | return 3; 116 | } else if (number.match(RegExp(words.four))) { 117 | return 4; 118 | } else if (number.match(RegExp(words.five))) { 119 | return 5; 120 | } else if (number.match(RegExp(words.six))) { 121 | return 6; 122 | } else if (number.match(RegExp(words.seven))) { 123 | return 7; 124 | } else if (number.match(RegExp(words.sun))) { 125 | return 7; 126 | } else if (number.match(RegExp(words.eight))) { 127 | return 8; 128 | } else if (number.match(RegExp(words.nine))) { 129 | return 9; 130 | } else if (number.match(RegExp(words.ten))) { 131 | return 10; 132 | } else { 133 | return null; 134 | } 135 | }; 136 | 137 | time2object = function(time) { 138 | var f, hour, match, minute, second; 139 | if (!time) { 140 | return null; 141 | } 142 | if (match = time.match(RegExp("(?:(" + words.numbers + "+)" + words.hour + "(" + words.numbers + "*)(?:" + words.minute + ")?(" + words.numbers + "*)(?:" + words.second + ")?)"))) { 143 | hour = number2integer(match[1]) || 0; 144 | minute = number2integer(match[2]) || 0; 145 | second = number2integer(match[3]) || 0; 146 | if (minute === 0.5) { 147 | minute = 30; 148 | } 149 | if (hour === 30) { 150 | hour = 0; 151 | minute += 30; 152 | } 153 | if ((f = hour % 1) > 0) { 154 | hour -= f; 155 | minute += 30; 156 | } 157 | while (second > 60) { 158 | second -= 60; 159 | minute += 1; 160 | } 161 | while (minute > 60) { 162 | minute -= 60; 163 | hour += 1; 164 | } 165 | return { 166 | hour: hour, 167 | minute: minute, 168 | second: second 169 | }; 170 | } else { 171 | return null; 172 | } 173 | }; 174 | 175 | dayTime2moment = function(daytime, timezone, moment) { 176 | var match, period, time; 177 | if (!daytime) { 178 | return null; 179 | } 180 | if (match = daytime.match(RegExp("(?:(?:(" + words.dayPeriods + ")?" + words.separator + "?(" + words.time + ") ?(" + words.dayPeriods + ")?)|(" + words.dayPeriods + "))"))) { 181 | if (period = match[4]) { 182 | moment = moment || Moment().tz(timezone); 183 | moment.endMoment = (moment != null ? moment.endMoment : void 0) || (moment && Moment(moment).tz(timezone)) || Moment().tz(timezone); 184 | moment.minute(0); 185 | moment.second(0); 186 | moment.endMoment.minute(0); 187 | moment.endMoment.second(0); 188 | if (period.match(RegExp(words.morning))) { 189 | moment.hour(9); 190 | moment.endMoment.hour(11); 191 | } else if (period.match(RegExp(words.noon))) { 192 | moment.hour(12); 193 | moment.endMoment.hour(13); 194 | } else if (period.match(RegExp(words.afternoon))) { 195 | moment.hour(14); 196 | moment.endMoment.hour(16); 197 | } else if (period.match(RegExp(words.night))) { 198 | moment.hour(18); 199 | moment.endMoment.hour(20); 200 | } else if (period.match(RegExp(words.midnight))) { 201 | moment.hour(0); 202 | moment.endMoment.hour(4); 203 | } 204 | return moment; 205 | } else { 206 | period = match[1] || match[3] || ''; 207 | time = time2object(match[2]); 208 | if (!time) { 209 | return null; 210 | } 211 | if (period.match(RegExp(words.pm)) && time.hour < 12) { 212 | time.hour += 12; 213 | } 214 | moment = moment || Moment().tz(timezone); 215 | if (time.hour < 12 && !period) { 216 | if (time.hour > (moment.hour() - 12) && time.hour < moment.hour()) { 217 | time.hour += 12; 218 | } 219 | } 220 | moment.hour(time.hour); 221 | moment.minute(time.minute || 0); 222 | moment.second(time.second || 0); 223 | return moment; 224 | } 225 | } else { 226 | return null; 227 | } 228 | }; 229 | 230 | date2object = function(date) { 231 | var c, day, match, month, now, year, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; 232 | if (!date) { 233 | return null; 234 | } 235 | if (match = date.match(RegExp("(?:(?:(?:(" + words.numbers + "+|" + words.this_previous_next + ") ?" + words.year + ")? ?(" + words.numbers + "+|" + words.separator + "|" + words.this_previous_next + ") ?" + words.month + ")? ?(?:(" + words.numbers + "+) ?" + words.day + "?))"))) { 236 | now = Moment(); 237 | year = number2integer(match[1]) || null; 238 | if (year && year < 100) { 239 | c = parseInt(now.year() / 100) * 100; 240 | year = year + c; 241 | } 242 | month = number2integer(match[2]) || null; 243 | day = number2integer(match[3]) || null; 244 | if ((_ref = match[1]) != null ? _ref.match(RegExp(words["this"])) : void 0) { 245 | year = now.year(); 246 | } else if ((_ref1 = match[1]) != null ? _ref1.match(RegExp(words.previous)) : void 0) { 247 | year = now.year() - 1; 248 | } else if ((_ref2 = match[1]) != null ? _ref2.match(RegExp(words.next)) : void 0) { 249 | year = now.year() + 1; 250 | } 251 | if ((_ref3 = match[2]) != null ? _ref3.match(RegExp(words["this"])) : void 0) { 252 | year = now.month() + 1; 253 | } else if ((_ref4 = match[2]) != null ? _ref4.match(RegExp(words.previous)) : void 0) { 254 | month = now.month(); 255 | } else if ((_ref5 = match[2]) != null ? _ref5.match(RegExp(words.next)) : void 0) { 256 | month = now.month() + 2; 257 | } 258 | return { 259 | year: year, 260 | month: month, 261 | day: day 262 | }; 263 | } else { 264 | return null; 265 | } 266 | }; 267 | 268 | dateExpression2moment = function(dateExp, timezone) { 269 | var dateObj, day, match, moment, _ref, _ref1; 270 | if (!dateExp) { 271 | return null; 272 | } 273 | moment = Moment().tz(timezone); 274 | moment.hour(0); 275 | moment.minute(0); 276 | moment.second(0); 277 | if (match = dateExp.match(RegExp("" + words.today + "[^" + words.year + "]*$"))) { 278 | return moment; 279 | } else if (match = dateExp.match(RegExp("" + words.tomorrow + "[^" + words.year + "]*$"))) { 280 | moment.date(moment.date() + 1); 281 | return moment; 282 | } else if (match = dateExp.match(RegExp(words.acquired))) { 283 | moment.date(moment.date() + 2); 284 | return moment; 285 | } else if (match = dateExp.match(RegExp(words.yesterday))) { 286 | moment.date(moment.date() - 1); 287 | return moment; 288 | } else if (match = dateExp.match(RegExp(words.the_day_before_yesterday))) { 289 | moment.date(moment.date() - 2); 290 | return moment; 291 | } else if (match = dateExp.match(RegExp("(" + words.this_previous_next + ")?" + words.week + "(" + words.weekdays + ")"))) { 292 | if ((_ref = match[1]) != null ? _ref.match(RegExp(words.previous)) : void 0) { 293 | moment.date(moment.date() - 7); 294 | } else if ((_ref1 = match[1]) != null ? _ref1.match(RegExp(words.next)) : void 0) { 295 | moment.date(moment.date() + 7); 296 | } 297 | day = number2integer(match[2]); 298 | if (match[2].match(RegExp(words.end))) { 299 | day = 6; 300 | } 301 | if (day >= 7) { 302 | day = 0; 303 | } 304 | moment.day(day); 305 | if (match[2].match(RegExp(words.end))) { 306 | moment.endMoment = Moment(moment).tz(timezone); 307 | moment.endMoment.date(moment.date() + 1); 308 | } 309 | return moment; 310 | } else if (match = dateExp.match(RegExp(words.like_date))) { 311 | dateObj = date2object(match[0]); 312 | if (!dateObj) { 313 | return null; 314 | } 315 | if (dateObj.day) { 316 | moment.date(dateObj.day); 317 | } 318 | if (dateObj.month) { 319 | moment.month(dateObj.month - 1); 320 | } 321 | if (dateObj.year) { 322 | moment.year(dateObj.year); 323 | } 324 | return moment; 325 | } else { 326 | return null; 327 | } 328 | }; 329 | 330 | expressions = []; 331 | 332 | expressions.push(function(text, timezone) { 333 | var date, eventName, location, match, moment, time; 334 | if (match = text.match(RegExp("^(?:([^" + words.separators + "]+) ?" + words.event_postfix + " ?)?(" + words.time + ")" + words.after + " ?(?:" + words.at + " ?([^" + words.separators + "]+))? ?(?:" + words.event_prefix + words.separator + "?([^" + words.separators + "]+))?$"))) { 335 | time = time2object(match[2]); 336 | location = match[3]; 337 | eventName = match[1] || match[4]; 338 | moment = Moment().tz(timezone); 339 | moment.hour(moment.hour() + time.hour); 340 | moment.minute(moment.minute() + time.minute); 341 | moment.second(moment.second() + time.second); 342 | date = moment.toDate(); 343 | if (location) { 344 | date.location = location; 345 | } 346 | if (eventName) { 347 | date.eventName = eventName; 348 | } 349 | return date; 350 | } 351 | }); 352 | 353 | expressions.push(function(text, timezone) { 354 | var date, eventName, fullDay, location, match, moment; 355 | if (match = text.match(RegExp("^(?:([^" + words.separators + "]+) ?" + words.event_postfix + ")? ?(" + words.dateExpression + ") ?(" + words.dayTime + ")? ?(?:" + words.to + " ?(" + words.dateExpression + ") ?(" + words.dayTime + ")? ?) ?(?:" + words.at + " ?([^" + words.separators + "]+))? ?(?:" + words.event_prefix + words.separator + "?([^" + words.separators + "]+))?$"))) { 356 | moment = dateExpression2moment(match[2], timezone) || Moment().tz(timezone); 357 | if (match[3]) { 358 | moment = dayTime2moment(match[3], timezone, moment); 359 | } 360 | if (!match[3] && !match[5]) { 361 | fullDay = true; 362 | } 363 | location = match[6]; 364 | eventName = match[1] || match[7]; 365 | if (match[4]) { 366 | moment.endMoment = dateExpression2moment(match[4], timezone); 367 | } 368 | if (moment.endMoment && match[5]) { 369 | moment.endMoment = dayTime2moment(match[5], timezone, moment.endMoment); 370 | } else if (moment.endMoment && !match[5]) { 371 | moment.endMoment.hour(23); 372 | moment.endMoment.minute(59); 373 | moment.endMoment.second(59); 374 | } 375 | date = moment.toDate(); 376 | if (moment.endMoment) { 377 | date.endTime = moment.endMoment.toDate(); 378 | } 379 | if (location) { 380 | date.location = location; 381 | } 382 | if (eventName) { 383 | date.eventName = eventName; 384 | } 385 | if (fullDay) { 386 | date.fullDay = fullDay; 387 | } 388 | return date; 389 | } 390 | }); 391 | 392 | expressions.push(function(text, timezone) { 393 | var date, eventName, location, match, moment; 394 | if (match = text.match(RegExp("^(?:([^" + words.separators + "]+) ?" + words.event_postfix + ")? ?(?:(" + words.dateExpression + ")) ?(" + words.dayTime + ") ?(?:" + words.to + " ?(" + words.dayTime + ") ?)? ?(?:" + words.at + " ?([^" + words.separators + "]+))? ?(?:" + words.event_prefix + words.separator + "?([^" + words.separators + "]+))?$"))) { 395 | moment = dateExpression2moment(match[2], timezone) || Moment().tz(timezone); 396 | if (match[3]) { 397 | moment = dayTime2moment(match[3], timezone, moment); 398 | } 399 | if (match[4]) { 400 | moment.endMoment = dayTime2moment(match[4], timezone, Moment(moment)); 401 | } 402 | location = match[5]; 403 | eventName = match[1] || match[6]; 404 | date = moment.toDate(); 405 | if (location) { 406 | date.location = location; 407 | } 408 | if (eventName) { 409 | date.eventName = eventName; 410 | } 411 | if (moment.endMoment) { 412 | date.endTime = moment.endMoment.toDate(); 413 | } 414 | return date; 415 | } 416 | }); 417 | 418 | expressions.push(function(text, timezone) { 419 | var date, eventName, location, match, moment; 420 | if (match = text.match(RegExp("^(?:([^" + words.separators + "]+) ?" + words.event_postfix + ")? ?(" + words.dayTime + ") ?(?:" + words.to + " ?(" + words.dayTime + ") ?)? ?(?:" + words.at + " ?([^" + words.separators + "]+))? ?(?:" + words.event_prefix + words.separator + "?([^" + words.separators + "]+))?$"))) { 421 | if (match[2]) { 422 | moment = dayTime2moment(match[2], timezone); 423 | } 424 | moment.endMoment = dayTime2moment(match[3], timezone); 425 | location = match[4]; 426 | eventName = match[1] || match[5]; 427 | date = moment.toDate(); 428 | if (location) { 429 | date.location = location; 430 | } 431 | if (eventName) { 432 | date.eventName = eventName; 433 | } 434 | if (moment.endMoment) { 435 | date.endTime = moment.endMoment.toDate(); 436 | } 437 | return date; 438 | } 439 | }); 440 | 441 | expressions.push(function(text, timezone) { 442 | var date, eventName, fullDay, location, match, moment; 443 | if (match = text.match(RegExp("^(?:([^" + words.separators + "]+) ?" + words.event_postfix + ")? ?(" + words.dateExpression + ") ?(" + words.dayTime + ")? ?(?:" + words.to + " ? (" + words.dateExpression + ") ?(" + words.dayTime + ")? ?)? ?(?:" + words.at + " ?([^" + words.separators + "]+))? ?(?:" + words.event_prefix + words.separator + "?([^" + words.separators + "]+))?$"))) { 444 | location = match[6]; 445 | eventName = match[1] || match[7]; 446 | moment = dateExpression2moment(match[2], timezone) || Moment().tz(timezone); 447 | if (match[3]) { 448 | moment = dayTime2moment(match[3], timezone, moment); 449 | } 450 | if (!match[3] && !match[5]) { 451 | fullDay = true; 452 | } 453 | if (match[5]) { 454 | moment.endMoment = dateExpression2moment(match[5], timezone, moment.endMoment); 455 | } 456 | if (moment.endMoment && match[5]) { 457 | moment.endMoment = dayTime2moment(match[5], timezone, moment.endMoment); 458 | } else if (moment.endMoment && !match[5]) { 459 | moment.endMoment.hour(23); 460 | moment.endMoment.minute(59); 461 | moment.endMoment.second(59); 462 | } 463 | date = moment.toDate(); 464 | if (location) { 465 | date.location = location; 466 | } 467 | if (eventName) { 468 | date.eventName = eventName; 469 | } 470 | if (moment.endMoment) { 471 | date.endTime = moment.endMoment.toDate(); 472 | } 473 | if (fullDay) { 474 | date.fullDay = fullDay; 475 | } 476 | return date; 477 | } 478 | }); 479 | 480 | module.exports = { 481 | words: words, 482 | expressions: expressions, 483 | number2integer: number2integer, 484 | time2object: time2object, 485 | dayTime2moment: dayTime2moment, 486 | date2object: date2object, 487 | dateExpression2moment: dateExpression2moment, 488 | testStrings: ['明天晚上到下禮拜六早上要開會', '10月10號 早上 10:00 ~ 下午 4:00', '十月十號'] 489 | }; 490 | 491 | }).call(this); 492 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "date-parser", 3 | "version": "0.0.7", 4 | "description": "將口語的日期時間解析成 Date 物件。", 5 | "main": "date-parser.js", 6 | "scripts": { 7 | "build-js": "coffee -c lib ; coffee -c date-parser.coffee ; coffee -c date-parser-browserify.coffee && browserify date-parser-browserify.js | uglifyjs > script/date-parser.min.js", 8 | "clean": "rm *.js ; rm lib/*.js", 9 | "test": "mocha --compilers coffee:coffee-script/register" 10 | }, 11 | "author": "Neson", 12 | "license": "MIT", 13 | "dependencies": { 14 | "browserify": "^6.2.0", 15 | "moment-timezone": "^0.2.4", 16 | "uglify-js": "^2.4.15" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/Neson/date-parser.git" 21 | }, 22 | "devDependencies": { 23 | "coffee-script": "^1.8.0", 24 | "mocha": "^2.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/test-helpers.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | dateToUnix: (date) -> 3 | return parseInt(date.getTime()/1000) if date 4 | return null 5 | -------------------------------------------------------------------------------- /test/zh-TW.coffee: -------------------------------------------------------------------------------- 1 | assert = require("assert") 2 | dateParser = require("../date-parser") 3 | Moment = require('moment-timezone') 4 | dateToUnix = require('./test-helpers').dateToUnix 5 | 6 | describe "dateParser_zh-TW", -> 7 | dateParser.locale 'zh-TW' 8 | 9 | describe "inTaipei", -> 10 | timezone = 'Asia/Taipei' 11 | 12 | it "should recognize 今天", -> 13 | assert.equal( 14 | dateToUnix dateParser.parse '今天', timezone 15 | Moment.tz(timezone).hour(0).minute(0).second(0).unix() 16 | ) 17 | assert.equal( 18 | dateParser.parse('今天').fullDay 19 | true 20 | ) 21 | 22 | it "should recognize 今天早上", -> 23 | assert.equal( 24 | dateToUnix dateParser.parse '今天早上', timezone 25 | Moment.tz(timezone).hour(9).minute(0).second(0).unix() 26 | ) 27 | assert.equal( 28 | dateToUnix dateParser.parse('今天早上', timezone).endTime 29 | Moment.tz(timezone).hour(11).minute(0).second(0).unix() 30 | ) 31 | 32 | it "should recognize 明天中午", -> 33 | assert.equal( 34 | dateToUnix dateParser.parse '明天中午', timezone 35 | Moment.tz(timezone).add(1, 'day').hour(12).minute(0).second(0).unix() 36 | ) 37 | assert.equal( 38 | dateToUnix dateParser.parse('明天中午', timezone).endTime 39 | Moment.tz(timezone).add(1, 'day').hour(13).minute(0).second(0).unix() 40 | ) 41 | 42 | it "should recognize 後天晚上", -> 43 | assert.equal( 44 | dateToUnix dateParser.parse '後天晚上', timezone 45 | Moment.tz(timezone).add(2, 'day').hour(18).minute(0).second(0).unix() 46 | ) 47 | assert.equal( 48 | dateToUnix dateParser.parse('後天晚上').endTime 49 | Moment.tz(timezone).add(2, 'day').hour(20).minute(0).second(0).unix() 50 | ) 51 | 52 | it "should recognize 今早八點", -> 53 | assert.equal( 54 | dateToUnix dateParser.parse '今早八點', timezone 55 | Moment.tz(timezone).hour(8).minute(0).second(0).unix() 56 | ) 57 | 58 | it "should recognize 明晚九點", -> 59 | assert.equal( 60 | dateToUnix dateParser.parse '明晚九點', timezone 61 | Moment.tz(timezone).add(1, 'day').hour(21).minute(0).second(0).unix() 62 | ) 63 | 64 | it "should recognize 一個半小時後", -> 65 | assert.equal( 66 | dateToUnix dateParser.parse '一個半小時後', timezone 67 | Moment.tz(timezone).add(1, 'hours').add(30, 'minutes').unix() 68 | ) 69 | 70 | it "should recognize 大概兩小時又三十五分鐘後", -> 71 | assert.equal( 72 | dateToUnix dateParser.parse '大概兩小時又三十五分鐘後', timezone 73 | Moment.tz(timezone).add(2, 'hours').add(35, 'minutes').unix() 74 | ) 75 | 76 | it "should recognize 星期五晚上", -> 77 | assert.equal( 78 | dateToUnix dateParser.parse '星期五晚上', timezone 79 | Moment.tz(timezone).day(5).hour(18).minute(0).second(0).unix() 80 | ) 81 | assert.equal( 82 | dateToUnix dateParser.parse('星期五晚上').endTime 83 | Moment.tz(timezone).day(5).hour(20).minute(0).second(0).unix() 84 | ) 85 | 86 | it "should recognize 下禮拜四晚上八點 搶票", -> 87 | result = dateParser.parse '下禮拜四晚上八點 搶票', timezone 88 | assert.equal( 89 | dateToUnix result 90 | Moment.tz(timezone).add(1, 'week').day(4).hour(20).minute(0).second(0).unix() 91 | ) 92 | assert.equal result.eventName, '搶票' 93 | 94 | it "should recognize 這週末要喝茶", -> 95 | result = dateParser.parse '這週末要喝茶', timezone 96 | assert.equal( 97 | dateToUnix result 98 | Moment.tz(timezone).day(6).hour(0).minute(0).second(0).unix() 99 | ) 100 | assert.equal( 101 | dateToUnix result.endTime 102 | Moment.tz(timezone).add(1, 'week').day(0).hour(23).minute(59).second(59).unix() 103 | ) 104 | assert.equal result.fullDay, true 105 | assert.equal result.eventName, '喝茶' 106 | 107 | it "should recognize 二零一四年五月五日", -> 108 | result = dateParser.parse '二零一四年五月五日', timezone 109 | assert.equal( 110 | dateToUnix result 111 | Moment('2014-05-05').tz(timezone).hour(0).minute(0).second(0).unix() 112 | ) 113 | assert.equal result.fullDay, true 114 | 115 | it "should recognize 2010/2/28 晚上八點 @自由廣場", -> 116 | result = dateParser.parse '2010/2/28 晚上八點 @自由廣場', timezone 117 | assert.equal( 118 | dateToUnix result 119 | Moment('2010-02-28').tz(timezone).hour(20).minute(0).second(0).unix() 120 | ) 121 | assert.equal result.location, '自由廣場' 122 | 123 | it "should recognize 上週五早上九點到下週六晚上7點要去玩", -> 124 | result = dateParser.parse '上週五早上九點到下週六晚上7點要去玩', timezone 125 | assert.equal( 126 | dateToUnix result 127 | Moment.tz(timezone).subtract(1, 'week').day(5).hour(9).minute(0).second(0).unix() 128 | ) 129 | assert.equal( 130 | dateToUnix result.endTime 131 | Moment.tz(timezone).add(1, 'week').day(6).hour(19).minute(0).second(0).unix() 132 | ) 133 | assert.equal( 134 | result.eventName 135 | '去玩' 136 | ) 137 | 138 | it "should recognize 辦公時間 週五 6:00pm ~ 週六 10:00am", -> 139 | result = dateParser.parse '辦公時間 週五 6:00pm ~ 週六 10:00am', timezone 140 | assert.equal( 141 | dateToUnix result 142 | Moment.tz(timezone).day(5).hour(18).minute(0).second(0).unix() 143 | ) 144 | assert.equal( 145 | dateToUnix result.endTime 146 | Moment.tz(timezone).day(6).hour(10).minute(0).second(0).unix() 147 | ) 148 | assert.equal result.eventName, '辦公時間' 149 | 150 | it "should NOT recognize 阿啊啊嗚嗚嗚嗚", -> 151 | assert.equal( 152 | dateToUnix dateParser.parse('阿啊啊嗚嗚嗚嗚') 153 | null 154 | ) 155 | 156 | describe "inMonaco", -> 157 | timezone = 'Europe/Monaco' 158 | 159 | it "should recognize 下禮拜三 4:32pm ~ 6:17pm", -> 160 | assert.equal( 161 | dateToUnix dateParser.parse '下禮拜三 4:32pm ~ 6:17pm', timezone 162 | Moment.tz(timezone).add(1, 'week').day(3).hour(16).minute(32).second(0).unix() 163 | ) 164 | assert.equal( 165 | dateToUnix dateParser.parse('下禮拜三 4:32pm ~ 6:17pm', timezone).endTime 166 | Moment.tz(timezone).add(1, 'week').day(3).hour(18).minute(17).second(0).unix() 167 | ) 168 | 169 | it "should recognize 明年1/1 凌晨一點", -> 170 | assert.equal( 171 | dateToUnix dateParser.parse '明年1/1 凌晨一點', timezone 172 | Moment.tz(timezone).add(1, 'year').month(0).date(1).hour(1).minute(0).second(0).unix() 173 | ) 174 | 175 | it "should recognize 下午三點半在銀行有派對", -> 176 | result = dateParser.parse '下午三點半在銀行有派對', timezone 177 | assert.equal( 178 | dateToUnix result 179 | Moment.tz(timezone).hour(15).minute(30).second(0).unix() 180 | ) 181 | assert.equal( 182 | result.location 183 | '銀行' 184 | ) 185 | assert.equal( 186 | result.eventName 187 | '派對' 188 | ) 189 | 190 | describe "inNewYork", -> 191 | timezone = 'America/New_York' 192 | 193 | it "should recognize 下個月五號中午 12:00", -> 194 | assert.equal( 195 | dateToUnix dateParser.parse '下個月五號中午 12:00', timezone 196 | Moment.tz(timezone).add(1, 'month').date(5).hour(12).minute(0).second(0).unix() 197 | ) 198 | 199 | it "should recognize 星期五晚上到下禮拜六早上要一直玩一直玩一直玩", -> 200 | result = dateParser.parse '星期五晚上到下禮拜六早上要一直玩一直玩一直玩', timezone 201 | assert.equal( 202 | dateToUnix result 203 | Moment.tz(timezone).day(5).hour(18).minute(0).second(0).unix() 204 | ) 205 | assert.equal( 206 | dateToUnix result.endTime 207 | Moment.tz(timezone).add(1, 'weeks').day(6).hour(9).minute(0).second(0).unix() 208 | ) 209 | assert.equal result.eventName, '一直玩一直玩一直玩' 210 | 211 | it "should recognize 今天晚上在聽風家有披薩吃", -> 212 | result = dateParser.parse '今天晚上在聽風家有披薩吃', timezone 213 | assert.equal( 214 | dateToUnix result 215 | Moment.tz(timezone).hour(18).minute(0).second(0).unix() 216 | ) 217 | assert.equal( 218 | dateToUnix result.endTime 219 | Moment.tz(timezone).hour(20).minute(0).second(0).unix() 220 | ) 221 | assert.equal result.location, '聽風家' 222 | assert.equal result.eventName, '披薩吃' 223 | 224 | it "should recognize 明天中午到晚上要開會", -> 225 | result = dateParser.parse '明天中午到晚上要開會', timezone 226 | assert.equal( 227 | dateToUnix result 228 | Moment.tz(timezone).add(1, 'days').hour(12).minute(0).second(0).unix() 229 | ) 230 | assert.equal( 231 | dateToUnix result.endTime 232 | Moment.tz(timezone).add(1, 'days').hour(18).minute(0).second(0).unix() 233 | ) 234 | assert.equal result.eventName, '開會' 235 | --------------------------------------------------------------------------------