Repository: XiaoMi/MiNLP (language: python)

Issue:
十一点半到明天早上八点，只抽取了【十一点半到明天】
参考时间：2023-03-08 T10:00:00  
1、十一点半到明天早上八点，只抽取了【十一点半到明天】，预期抽取【十一点半到明天早上八点】  
2、十一点到明天八点/十四点到明天八点，输出的开始时间错误。预期是2023-03-08的11点和14点  

正常的case：  
十一点到八点  
今天十一点到八点



Discussion / hints:

Fix patch:
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/Rules.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/Rules.scala
index cec4e7f2..619fc7d6 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/Rules.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/Rules.scala
@@ -24,7 +24,6 @@ import com.xiaomi.duckling.dimension.matcher.{GroupMatch, RegexMatch}
 import com.xiaomi.duckling.dimension.ordinal.{Ordinal, OrdinalData}
 import com.xiaomi.duckling.dimension.time.Helpers._
 import com.xiaomi.duckling.dimension.time.Prods._
-import com.xiaomi.duckling.dimension.time.date.Date
 import com.xiaomi.duckling.dimension.time.duration.{Duration, DurationData, isFuzzyNotLatentDuration, isNotLatentDuration}
 import com.xiaomi.duckling.dimension.time.enums.{Grain, Hint, IntervalDirection}
 import com.xiaomi.duckling.dimension.time.enums.Grain._
@@ -44,7 +43,7 @@ trait Rules extends DimRules {
     pattern = List(
       "((这|今|本(?!现在))一?个?|明|上+一?个?|前一个?|(?<![一|查|看|搜|记|问|写])下+一?个?)".regex,
       // ❌ 下周一早上 =>  下[周一早上]
-      and(isDimension(Time), isNotLatent, not(isHint(Intersect)), not(isAPartOfDay)).predicate
+      and(isDimension(Time), isNotLatent, not(isHint(Intersect, FinalRule)), not(isAPartOfDay)).predicate
     ),
     prod = tokens {
       case Token(RegexMatch, GroupMatch(s :: _)) :: (t @ Token(Time, td: TimeData)) :: _ =>
@@ -126,13 +125,13 @@ trait Rules extends DimRules {
       name = "intersect",
       // 左右边均不能是中午/下午，避免与<dim time> <part-of-day>冲突
       pattern = List(
-        and(isDimension(Time), isNotLatent, not(isAPartOfDay)).predicate,
+        and(isDimension(Time), isNotLatent, not(isAPartOfDay), isNotHint(Hint.Season)).predicate,
         // "一日"单独是latent，但是可以参与组合
         and(isDimension(Time), or(and(or(isNotLatent, isLatent0oClockOfDay), not(isAPartOfDay)), isADayOfMonth)).predicate
       ),
       prod = tokens {
         case Token(Time, td1: TimeData) :: Token(Time, td2: TimeData) :: _
-            if td1.timeGrain > td2.timeGrain && !(td1.hint == Date && td2.hint == Date) =>
+            if td1.timeGrain > td2.timeGrain && !(td1.hint == Hint.Date && td2.hint == Hint.Date) =>
           // 破除(y-m)-d和y-(m-d)均构造出来的问题
           if (td1.hint == YearMonth && td2.hint == DayOnly) None
           // 固定顺序，避免(y-m)-(d H-M-S) 以及(y)-(m-d H-M-S)出现
@@ -270,7 +269,7 @@ trait Rules extends DimRules {
       case (options: Options, Token(Duration, DurationData(v, grain, false, _, _)) :: Token(_, GroupMatch(s :: _)) :: _) =>
         val offset = if (s.endsWith("后")) v else -v
         val roundGrain = if (options.timeOptions.inheritGrainOfDuration) grain else NoGrain
-        tt(cycleNth(grain, offset, roundGrain))
+        tt(finalRule(cycleNth(grain, offset, roundGrain)))
     }
   )
 
@@ -283,7 +282,7 @@ trait Rules extends DimRules {
     prod = optTokens {
       case (options: Options, _ :: Token(Duration, DurationData(v, grain, _, _, _)) :: _ ) =>
         val roundGrain = if (options.timeOptions.inheritGrainOfDuration) grain else NoGrain
-        tt(cycleNth(grain, v, roundGrain))
+        tt(finalRule(cycleNth(grain, v, roundGrain)))
     }
   )
 
@@ -296,7 +295,7 @@ trait Rules extends DimRules {
     prod = optTokens {
       case (options: Options, _ :: Token(Duration, DurationData(v, grain, _, _, _)) :: _ ) =>
         val roundGrain = if (options.timeOptions.inheritGrainOfDuration) grain else NoGrain
-        tt(cycleNth(grain, v, roundGrain))
+        tt(finalRule(cycleNth(grain, v, roundGrain)))
     }
   )
 
@@ -328,7 +327,7 @@ trait Rules extends DimRules {
 
   val ruleTimeBeforeOfAfter = Rule(
     name = "<time> before/after",
-    pattern = List(isDimension(Time).predicate, "[之以]?([前后])".regex),
+    pattern = List(and(not(isHint(Hint.UncertainRecent, Hint.Recent)), isNotLatent).predicate, "[之以]?([前后])".regex),
     prod = tokens {
       case Token(Time, td: TimeData) :: Token(_, GroupMatch(_ :: direction :: _)) :: _ =>
         val intervalDirection =
@@ -346,7 +345,7 @@ trait Rules extends DimRules {
     */
   val ruleTimeBeforeOfAfter2 = Rule(
     name = "<time> before/after 2",
-    pattern = List(isADayOfMonth.predicate, "的".regex, isNotLatentDuration.predicate, "[之以]?([前后])".regex),
+    pattern = List(and(isADayOfMonth, isNotLatent).predicate, "的".regex, isNotLatentDuration.predicate, "[之以]?([前后])".regex),
     prod = tokens {
       case Token(Time, td: TimeData) :: _ :: Token(Duration, DurationData(v, g, _, _, _)) :: Token(_, GroupMatch(_ :: d :: _)) :: _ =>
         val dv = if (d == "前") -v else v
@@ -359,7 +358,7 @@ trait Rules extends DimRules {
   
   val ruleTimeBeforeOfAfter3 = Rule(
     name = "<time> before/after 3",
-    pattern = List(isADayOfMonth.predicate, isNotLatentDuration.predicate, "[之以]?([前后])".regex),
+    pattern = List(and(isADayOfMonth, isNotLatent).predicate, isNotLatentDuration.predicate, "[之以]?([前后])".regex),
     prod = tokens {
       case Token(Time, td: TimeData) :: Token(Duration, DurationData(v, g, _, _, _)) :: Token(_, GroupMatch(_ :: d :: _)) :: _ =>
         val dv = if (d == "前") -v else v
@@ -405,13 +404,11 @@ trait Rules extends DimRules {
     prod = tokens {
       case Token(Time, td1 @ TimeData(pred1, _, g1, _, _, _, _, _, _, _, _, _)) :: _ ::
             Token(Time, td2 @ TimeData(pred2, _, g2, _, _, _, _, _, _, _, _, _)) :: _ =>
-        val grainCompare = for {
-          pg1 <- maxPredicateGrain(pred1)
-          pg2 <- maxPredicateGrain(pred2)
-        } yield pg1 >= pg2
         // 限定求交的Grain，避免日交月
+        val m1 = maxPredicateGrain(pred1)
+        val m2 = maxPredicateGrain(pred2)
         val isValid =
-          grainCompare.nonEmpty && grainCompare.get && (g1 < Day && g2 < Day || g1 >= Day && g2 >= Day) ||
+           (g1 < Day && g2 < Day || g1 >= Day && (g1 == g2 || m1.nonEmpty && m1 == m2)) ||
             g1 >= g2 ||
             td1.hint == RecentNominal || td2.hint == RecentNominal
         if (isValid) {
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/Types.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/Types.scala
index 103273ec..e917ac1f 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/Types.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/Types.scala
@@ -403,6 +403,18 @@ object Types {
         case None           => this
       }
     }
+
+    override def toString: String = {
+      end match {
+        case Some(e) => s"$start/$e($grain)"
+        case None =>
+          direction match {
+            case Some(IntervalDirection.Before) => s"(,$start]($grain)"
+            case Some(IntervalDirection.After) => s"[$start,)($grain)"
+            case _ => s"$start($grain)"
+          }
+      }
+    }
   }
 
   case class TimeContext(refTime: TimeObject,
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/enums/Hint.java b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/enums/Hint.java
index 45942172..838154e0 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/enums/Hint.java
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/enums/Hint.java
@@ -57,5 +57,9 @@ public enum Hint {
     /**
      * 求交的结果
      */
-    Intersect
+    Intersect,
+    /**
+     * 春夏秋冬
+     */
+    Season
 }
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/helper/TimeDataHelpers.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/helper/TimeDataHelpers.scala
index cca93e70..cb63082d 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/helper/TimeDataHelpers.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/helper/TimeDataHelpers.scala
@@ -233,7 +233,8 @@ object TimeDataHelpers {
 
   def interval1(intervalType: IntervalType, td1: TimeData, td2: TimeData): TimeData = {
     val it =
-      if (td1.timeGrain == td2.timeGrain && td1.timeGrain == Day) Closed
+      if (td1.hint == Hint.Season && td2.hint == Hint.Season) intervalType
+      else if (td1.timeGrain == td2.timeGrain && td1.timeGrain == Day) Closed
       else intervalType
     TimeData(
       timePred = mkTimeIntervalsPredicate(it, td1, td2),
@@ -249,4 +250,6 @@ object TimeDataHelpers {
       case _ => None // 不一致的先搁置
     }
   }
+
+  def finalRule(td: TimeData): TimeData = td.copy(hint = Hint.FinalRule)
 }
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/helper/TimeObjectHelpers.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/helper/TimeObjectHelpers.scala
index b9c9e4b2..28cacfdb 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/helper/TimeObjectHelpers.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/helper/TimeObjectHelpers.scala
@@ -109,7 +109,7 @@ object TimeObjectHelpers {
   @tailrec
   def timeIntersect(t1: TimeObject)(t2: TimeObject): Option[TimeObject] = {
     (t1, t2) match {
-      case (TimeObject(start1, g1, end1, _), TimeObject(start2, g2, _, _)) =>
+      case (TimeObject(start1, g1, end1, _), TimeObject(start2, g2, end2, _)) =>
         val e1 = timeEnd(t1)
         val e2 = timeEnd(t2)
         val s1 = timeStart(t1)
@@ -117,9 +117,9 @@ object TimeObjectHelpers {
         val gg = if (g1 > g2) g2 else g1
         if (s1.isAfter(s2)) timeIntersect(t2)(t1)
         else if (!e1.isAfter(s2)) None
-        else if (e1.isBefore(e2) || s1 == s2 && e1 == e2 && end1.nonEmpty) {
-          Some(TimeObject(start = s2, end = end1, grain = gg))
-        } else Some(t2.copy(grain = gg))
+        else if (e1.isBefore(e2)) Some(TimeObject(start = s2, end = end2, grain = gg))
+        else if(s1 == s2 && e1 == e2 && end1.nonEmpty) Some(TimeObject(start = s2, end = end1, grain = gg))
+        else Some(t2.copy(grain = gg))
     }
   }
 
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/package.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/package.scala
index 79db01da..e720db46 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/package.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/package.scala
@@ -378,10 +378,10 @@ package object time {
       val (past1, future1) = (applyF(firstPast), applyF(firstFuture))
 
       // Separate what's before and after now from the past's series
-      val (newFuture, stillPast) = past1.span(timeStartsBeforeTheEndOf(nowTime))
+      val (newFuture, stillPast) = past1.partition(timeStartsBeforeTheEndOf(nowTime))
       // A series that ends at the earliest time
       val oldPast = stillPast.takeWhile(timeStartsBeforeTheEndOf(context.minTime))
-      val (newPast, stillFuture) = future1.span(t => !timeStartsBeforeTheEndOf(nowTime)(t))
+      val (newPast, stillFuture) = future1.partition(t => !timeStartsBeforeTheEndOf(nowTime)(t))
       // A series that ends at the furthest future time
       val oldFuture = stillFuture.takeWhile(timeStartsBeforeTheEndOf(_)(context.maxTime))
 
@@ -395,7 +395,7 @@ package object time {
       val past = sortedPast ++ oldPast
       // Future is the future from the past's series with the
       // future from the future's series tacked on
-      val future = sortedFuture ++ oldFuture
+      val future = oldFuture ++ sortedFuture
       (past, future)
     }
 
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/Dates.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/Dates.scala
index 34116486..493714db 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/Dates.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/Dates.scala
@@ -24,7 +24,7 @@ import com.xiaomi.duckling.dimension.time.{Time, TimeData}
 
 object Dates {
   val ruleSimpleDates = Rule(name = "<date>", pattern = List(isDimension(Date).predicate), prod = tokens {
-    case Token(_, td: TimeData) :: _ => Token(Time, td.copy(hint = Hint.Date))
+    case Token(_, td: TimeData) :: _ => Token(Time, td.copy(hint = if (td.hint == Hint.Season) td.hint else Hint.Date))
   })
 
   val rules = List(ruleSimpleDates)
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/SolarTerms.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/SolarTerms.scala
index 7a5299f1..a2196a01 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/SolarTerms.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/SolarTerms.scala
@@ -74,18 +74,10 @@ object SolarTerms {
             case _    => ("冬季", "立冬", "立春")
           }
   
-          val from = TimeData(timePred = solarTermPredicate(ss), timeGrain = Day, okForThisNext = true, holiday = ss) // 开始日期
-          val to = TimeData( // 结束日期
-            SequencePredicate(List(
-              TimeData(timePred = solarTermPredicate(se), timeGrain = Day, okForThisNext = true, holiday = se),
-              cycleNth(Day, -1).at(Hint.Recent) // 后开区间，需要减去一天
-            )),
-            timeGrain = Day,
-            hint = Hint.Sequence,
-            calendar = from.calendar
-          )
+          val from = TimeData(timePred = solarTermPredicate(ss), timeGrain = Day, okForThisNext = true, holiday = ss, hint = Hint.Season)
+          val to = TimeData(timePred = solarTermPredicate(se), timeGrain = Day, okForThisNext = true, holiday = se, hint = Hint.Season)
   
-          for (td <- interval(Open, from, to)) yield Token(Date, td.copy(holiday = holiday))
+          for (td <- interval(Open, from, to)) yield Token(Date, td.copy(holiday = holiday, hint = Hint.Season))
         } else None
     })
 
diff --git a/duckling-fork-chinese/learning/src/main/scala/com/xiaomi/duckling/dimension/time/Examples.scala b/duckling-fork-chinese/learning/src/main/scala/com/xiaomi/duckling/dimension/time/Examples.scala
index 21ec906b..b64ec01d 100644
--- a/duckling-fork-chinese/learning/src/main/scala/com/xiaomi/duckling/dimension/time/Examples.scala
+++ b/duckling-fork-chinese/learning/src/main/scala/com/xiaomi/duckling/dimension/time/Examples.scala
@@ -383,6 +383,14 @@ object Examples extends DimExamples {
       ),
       List("明天下午三点到6点")
     ),
+    (
+      localDateTimeInterval(
+        LocalDateTime.of(2013, 2, 12, 11, 30, 0),
+        LocalDateTime.of(2013, 2, 13, 8, 0, 0),
+        Minute
+      ),
+      List("十一点半到明天早上八点")
+    ),
     (datetime(LocalDateTime.of(2013, 3, 12, 19, 0, 0), Hour), List("一个月后下午19点")),
     (datetime(LocalDateTime.of(2018, 3, 30, 10, 10, 0), Minute), List("五年后3月三十号10点十分")),
     (
@@ -410,6 +418,14 @@ object Examples extends DimExamples {
       ),
       List("今天上午到明天下午")
     ),
+    (
+      datetimeInterval(
+        new DuckDateTime(LocalDateTime.of(1777, 10, 30, 0, 0, 0)),
+        new DuckDateTime(LocalDateTime.of(1800, 1, 1, 0, 0, 0)),
+        Day
+      ),
+      List("一七七七年十月三十日到一八零零年")
+    ),
     (
       lunarDateTimeInterval(
         new LunarCalendar(2019, 12, 1, false),
diff --git a/duckling-fork-chinese/learning/src/main/scala/com/xiaomi/duckling/task/NaiveBayesConsole.scala b/duckling-fork-chinese/learning/src/main/scala/com/xiaomi/duckling/task/NaiveBayesConsole.scala
index a05ac2a7..6160305f 100644
--- a/duckling-fork-chinese/learning/src/main/scala/com/xiaomi/duckling/task/NaiveBayesConsole.scala
+++ b/duckling-fork-chinese/learning/src/main/scala/com/xiaomi/duckling/task/NaiveBayesConsole.scala
@@ -73,7 +73,8 @@ object NaiveBayesConsole extends LazyLogging {
         "winner-only",
         "with-latent",
         "full",
-        "inherit-duration-grain"
+        "inherit-duration-grain",
+        "seasons"
       ),
       NullCompleter.INSTANCE
     )
@@ -119,6 +120,7 @@ object NaiveBayesConsole extends LazyLogging {
               case "with-latent" => options.copy(withLatent = value)
               case "full" => options.copy(full = value)
               case "inherit-duration-grain" => options.timeOptions.setInheritGrainOfDuration(value); options
+              case "seasons" => options.timeOptions.setParseFourSeasons(true); options
               case _ => options
             }
             opt


Test patch:
diff --git a/duckling-fork-chinese/core/src/test/scala/com/xiaomi/duckling/dimension/time/TimeTest.scala b/duckling-fork-chinese/core/src/test/scala/com/xiaomi/duckling/dimension/time/TimeTest.scala
index c957ba36..7156a80a 100644
--- a/duckling-fork-chinese/core/src/test/scala/com/xiaomi/duckling/dimension/time/TimeTest.scala
+++ b/duckling-fork-chinese/core/src/test/scala/com/xiaomi/duckling/dimension/time/TimeTest.scala
@@ -104,6 +104,13 @@ class TimeTest extends UnitSpec {
         }
       }.headOption should contain (2021)
     }
+
+    it("不支持日到月等") {
+      val cases = Table("query", "八号到十二月", "八点到五分", "春天一点", "这十分钟后", "十后", "后三十分钟后")
+      forAll(cases) { query =>
+        parse(query, context = testContext, options = options) should have size 0
+      }
+    }
   }
 
   describe("TimeSimpleTest") {