中文對話中的日期表達方式有不少java
大概總結了一下大概右下邊幾類git
// 昨天 今天 明天 // 前天 大前天 後天 大後天 大大前天 大大後天。。。 // 2號 15日 五月50號 // 下個月五號 上個月今天 // 這週末 這週五 下週六 上週日 // 10天后 五天前
那麼找規律,將有共同特性的結構進行統一處理github
總結一下共同特性正則表達式
1. 漢字正整數轉化成阿拉伯數字測試
// 1. 10天后 五十天前 十二月十五日 十二月13號 這些能夠提取出將漢字數字轉化成阿拉伯數字的方法
對話中不多出現「二十萬天前」這種比較離譜的日子,因此在這裏我只實現了一個萬之內正整數轉換的方法code
在整個處理過程當中考慮了不少,好比說「兩萬二」這種數字orm
private static final HashMap<Character, Integer> NUMBER_MAPPER = new HashMap<Character, Integer>() {{ put('一', 1); put('二', 2); put('三', 3); put('四', 4); put('五', 5); put('六', 6); put('七', 7); put('八', 8); put('九', 9); put('零', 0); put('壹', 1); put('貳', 2); put('叄', 3); put('肆', 4); put('伍', 5); put('陸', 6); put('柒', 7); put('捌', 8); put('玖', 9); put('〇', 0); put('兩', 2); put('倆', 2); put('倆', 2); put('仨', 3); put('1', 1); put('2', 2); put('3', 3); put('4', 4); put('5', 5); put('6', 6); put('7', 7); put('8', 8); put('9', 9); put('0', 0); }}; private static final String NUMBER_PATTERN = "(一|二|三|四|五|六|七|八|九|壹|貳|叄|肆|五|陸|柒|捌|玖|兩|倆|倆|仨)"; //口語數字轉化爲標準數字的正則表達式 private static final HashMap<String, String> NUMBER_SPOKEN_PATTERN = new HashMap<String, String>() {{ put(NUMBER_PATTERN + "{1}(百|佰)" + NUMBER_PATTERN + "{1}$", "十"); put(NUMBER_PATTERN + "{1}(千|仟)" + NUMBER_PATTERN + "{1}$", "百"); put(NUMBER_PATTERN + "{1}(萬|萬)" + NUMBER_PATTERN + "{1}$", "千"); }}; private NumberUtils() {} /** * 字符串數字轉阿拉伯數字,支持萬之內正整數漢語數字轉換 * @param input * @return */ public static Integer parseInteger(String input) { try { return Integer.parseInt(input); } catch (NumberFormatException e) { return hanNumber2Arabic(input); } } /** * 萬之內中國數字轉阿拉伯數字(正整數) * 5百 -> 500 * 六十一 -> 61 * @param input * @return */ private static Integer hanNumber2Arabic(String input) { if (isEmpty(input)) { return null; } input = regular(input); Queue<Integer> stash = new LinkedList<>(); Integer result = 0; for (char c : input.toCharArray()) { switch (c) { case '零': case '〇': break; case '十': case '拾': result += stash.poll() * 10; break; case '百': case '佰': result += stash.poll() * 100; break; case '千': case '仟': result += stash.poll() * 1000; break; case '萬': case '萬': result += stash.poll() * 10000; break; default: stash.offer(NUMBER_MAPPER.get(c)); break; } } if (stash.size() > 0) { result += stash.poll(); } return result; } /** * 萬之內口語數字轉化爲標準數字 * 兩千二 -> 兩千二百 * 兩萬五 -> 兩萬五千 * * @param input */ private static String regular(String input) { for (Map.Entry<String, String> entry : NUMBER_SPOKEN_PATTERN.entrySet()) { Pattern pat = Pattern.compile(entry.getKey()); Matcher matcher = pat.matcher(input); if (matcher.find()) { return input + entry.getValue(); } } return input; }
2. 中文的月表述和日表述rem
// 在中文中表示月的說法:這個月 上個月 五月 5月 大上個月。。。 // 一樣在中文中表示日的說法:今天 昨天 大前天 大大後天 5號 18日。。。 // 能夠提取出一個識別這些表述的方法
在這裏我沒有實現「X天前」,「上週末」,「星期二」這些表述;只實現了針對於上邊表述的識別字符串
不管是日表述仍是月表述均可以分爲兩類get
1. 針對於當前時間的表述 好比說:這個月 昨天
2. 針對於具體月份或日的表示 好比說:3月 六號
因此代碼以下
private static final HashMap<String, Integer> CHINESE_DAY_EXPRESSION_MAPPER = new HashMap<String, Integer>() {{ put("今天", 0); put("明天", 1); put("後天", 2); put("昨天", -1); put("前天", -2); }}; private static final HashMap<String, Integer> CHINESE_MONTH_EXPRESSION_MAPPER = new HashMap<String, Integer>() {{ put("這個月", 0); put("下個月", 1); put("上個月", -1); }}; private DateUtils() {} /** * 根據月表述和日表述 計算日期 * @param monthExpression 月表述:"這個月" "上個月" "五月" "5月" * @param dayExpression 日表述:"今天" "前天" "5號" "5日" * @return 計算後的日期 這個月五號 -> 2017-11-05 */ public static LocalDate dateExcursion(String monthExpression, String dayExpression) { monthExpression = Utils.isNull(monthExpression, ""); dayExpression = Utils.isNull(dayExpression, ""); LocalDate result = LocalDate.now(ZoneId.systemDefault()); int monthOffset = getDateAdjunct(monthExpression); if (monthOffset != 0) { monthExpression = monthExpression.substring(monthOffset); } // 對於不一樣的表述 不一樣處理 if (CHINESE_MONTH_EXPRESSION_MAPPER.get(monthExpression) != null) { int baseOffset = CHINESE_MONTH_EXPRESSION_MAPPER.get(monthExpression); if (baseOffset > 0) { // 針對基於當前日期的表述 進行加或減 result = result.plusMonths(baseOffset + monthOffset); } else if (baseOffset != 0) { result = result.plusMonths(baseOffset - monthOffset); } } else if (!Utils.isEmpty(monthExpression)) { // 針對指定日期的表述 進行指定 Integer month = NumberUtils.parseInteger(monthExpression.substring(0, monthExpression.length() - 1)); result = result.withMonth(month); } int dayOffset = getDateAdjunct(dayExpression); if (dayOffset != 0) { dayExpression = dayExpression.substring(dayOffset); } if (CHINESE_DAY_EXPRESSION_MAPPER.get(dayExpression) != null) { int baseOffset = CHINESE_DAY_EXPRESSION_MAPPER.get(dayExpression); if (baseOffset > 0) { result = result.plusDays(baseOffset + dayOffset); } else if (baseOffset != 0) { result = result.plusDays(baseOffset - dayOffset); } } else if (!Utils.isEmpty(dayExpression)) { Integer dayOfMonth = NumberUtils.parseInteger(dayExpression.substring(0, dayExpression.length() - 1)); result = result.withDayOfMonth(dayOfMonth); } return result; } // 在這裏處理用‘大’字修飾的表述 處理的有點笨拙 private static Integer getDateAdjunct (String dateExpression) { int result = 0; for (char c : dateExpression.toCharArray()) { if (c == '大') { result++; } else { break; } } return result; }
3. 最後就是須要在分詞的時候把這些表述準確的分出來,在這裏我用的是HanLP分詞,屏蔽了數量詞識別,加入了本身的專門用於日期識別的方法,同時爲了避免污染HanLP優秀的詞庫,加入了本身定義的專門用於日期識別的詞庫
今天 t_day 1024 昨天 t_day 1024 明天 t_day 1024 上個月 t_month 1024 下個月 t_month 1024 這個月 t_month 1024 一月 t_month 1024 二月 t_month 1024 三月 t_month 1024 四月 t_month 1024 五月 t_month 1024
那麼有了本身的時間詞和詞性,就能夠方便的識別這些時間表述了
/** * 從中文輸入中提取具體日期信息 * @param input "上個月今天" "這個月6號" "明天" "5月6號" * @return 2017-05-06 */ public static String getAppointDay(String input) { List<Term> terms = NLPSegment.seg(input); dateMerge(terms); String monthExpression = ""; String dayExpression = ""; for (int i = 0; i < terms.size(); i++) { Term term = terms.get(i); if ("t_month".equals(term.nature.toString())) { monthExpression = term.word; } else if ("t_day".equals(term.nature.toString())) { dayExpression = term.word; return DateUtils.dateExcursion(monthExpression, dayExpression).toString(); } } return DateUtils.dateExcursion(monthExpression, dayExpression).toString(); } /** * 合併數量詞和日期單位 * 5/m 月/q -> 5月/mq * 大/a 前天/t_day -> 大前天/t_day */ public static void dateMerge(List<Term> terms) { Term prev = null; Iterator<Term> iterator = terms.iterator(); while (iterator.hasNext()) { Term current = iterator.next(); if ("月".equals(current.word) && prev != null && "m".equals(prev.nature.toString())) { prev.word += current.word; prev.nature = Nature.create("t_month"); iterator.remove(); } else if (("日".equals(current.word) || "號".equals(current.word)) && prev != null && "m".equals(prev.nature.toString())) { prev.word += current.word; prev.nature = Nature.create("t_day"); iterator.remove(); } else if (("大".equals(current.word) || "大大".equals(current.word)) && prev != null && prev.word != null && (prev.word.startsWith("大") && prev.word.endsWith("大"))) { prev.word += current.word; prev.nature = Nature.a; iterator.remove(); } else if (("t_month".equals(current.nature.toString()) || "t_day".equals(current.nature.toString())) && prev != null && prev.word != null && (prev.word.startsWith("大") && prev.word.endsWith("大"))) { prev.word += current.word; prev.nature = Nature.create("t_month".equals(current.nature.toString()) ? "t_month" : "t_day"); iterator.remove(); } else { prev = current; } } }
dateMerge方法有兩個做用
1. 用於合併數字和時間單位
2. 用於合併用‘大’修飾的時間表述
最後調用getAppointDay()方法進行分析便可
測試一下
public void testGetAppointDay() { LocalDate now = LocalDate.now(ZoneId.systemDefault()); assertEquals(HanLPUtils.getAppointDay("昨每天氣怎麼樣"), now.plusDays(-1).toString()); assertEquals(HanLPUtils.getAppointDay("前每天氣怎麼樣"), now.plusDays(-2).toString()); assertEquals(HanLPUtils.getAppointDay("今每天氣怎麼樣"), now.toString()); assertEquals(HanLPUtils.getAppointDay("明每天氣怎麼樣"), now.plusDays(1).toString()); assertEquals(HanLPUtils.getAppointDay("後每天氣怎麼樣"), now.plusDays(2).toString()); assertEquals(HanLPUtils.getAppointDay("大後每天氣怎麼樣"), now.plusDays(3).toString()); assertEquals(HanLPUtils.getAppointDay("大大後每天氣怎麼樣"), now.plusDays(4).toString()); assertEquals(HanLPUtils.getAppointDay("大大大後每天氣怎麼樣"), now.plusDays(5).toString()); assertEquals(HanLPUtils.getAppointDay("大大大大後每天氣怎麼樣"), now.plusDays(6).toString()); assertEquals(HanLPUtils.getAppointDay("大大前每天氣怎麼樣"), now.plusDays(-4).toString()); assertEquals(HanLPUtils.getAppointDay("大前每天氣怎麼樣"), now.plusDays(-3).toString()); assertEquals(HanLPUtils.getAppointDay("大下個月後每天氣怎麼樣"), now.plusMonths(2).plusDays(2).toString()); assertEquals(HanLPUtils.getAppointDay("大上個月後每天氣怎麼樣"), now.plusMonths(-2).plusDays(2).toString()); assertEquals(HanLPUtils.getAppointDay("下個月後每天氣怎麼樣"), now.plusMonths(1).plusDays(2).toString()); assertEquals(HanLPUtils.getAppointDay("上個月今每天氣怎麼樣"), now.plusMonths(-1).toString()); assertEquals(HanLPUtils.getAppointDay("下個月前每天氣怎麼樣"), now.plusMonths(1).plusDays(-2).toString()); assertEquals(HanLPUtils.getAppointDay("上個月昨每天氣怎麼樣"), now.plusMonths(-1).plusDays(-1).toString()); assertEquals(HanLPUtils.getAppointDay("這個月今每天氣怎麼樣"), now.toString()); assertEquals(HanLPUtils.getAppointDay("五月二十日天氣怎麼樣"), now.withMonth(5).withDayOfMonth(20).toString()); assertEquals(HanLPUtils.getAppointDay("5月二十日天氣怎麼樣"), now.withMonth(5).withDayOfMonth(20).toString()); assertEquals(HanLPUtils.getAppointDay("五月20日天氣怎麼樣"), now.withMonth(5).withDayOfMonth(20).toString()); assertEquals(HanLPUtils.getAppointDay("5月20日天氣怎麼樣"), now.withMonth(5).withDayOfMonth(20).toString()); assertEquals(HanLPUtils.getAppointDay("20號天氣怎麼樣"), now.withDayOfMonth(20).toString()); assertEquals(HanLPUtils.getAppointDay("二十號天氣怎麼樣"), now.withDayOfMonth(20).toString()); assertEquals(HanLPUtils.getAppointDay("大上個月大大大大大前每天氣怎麼樣"), now.plusMonths(-2).plusDays(-7).toString()); assertEquals(HanLPUtils.getAppointDay("天氣怎麼樣"), now.toString()); }
結果固然是經過了