中文对话中的日期表达方式有很多
大概总结了一下大概右下边几类
// 昨天 今天 明天
// 前天 大前天 后天 大后天 大大前天 大大后天。。。
// 2号 15日 五月50号
// 下个月五号 上个月今天
// 这周末 这周五 下周六 上周日
// 10天后 五天前
那么找规律,将有共同特性的结构进行统一处理
总结一下共同特性
1. 汉字正整数转化成阿拉伯数字
// 1. 10天后 五十天前 十二月十五日 十二月13号 这些可以提取出将汉字数字转化成阿拉伯数字的方法
对话中很少出现“二十万天前”这种比较离谱的日子,所以在这里我只实现了一个万以内正整数转换的方法
在整个处理过程中考虑了很多,比如说“两万二”这种数字
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. 中文的月表述和日表述
// 在中文中表示月的说法:这个月 上个月 五月 5月 大上个月。。。
// 同样在中文中表示日的说法:今天 昨天 大前天 大大后天 5号 18日。。。
// 可以提取出一个识别这些表述的方法
在这里我没有实现“X天前”,“上周末”,“星期二”这些表述;只实现了针对于上边表述的识别
不论是日表述还是月表述都可以分为两类
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());
}
结果当然是通过了