一種識別對話中日期的方法

中文對話中的日期表達方式有不少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());
    }

結果固然是經過了

相關文章
相關標籤/搜索