Repository: XiaoMi/MiNLP (language: python)

Issue:
[DUCK] interval类型时间偏移，支持指定参考的时间点
如token=晚上【[18:00, 00:00)】，或token=18点到20点  
默认：当前时间为19:00，超过了interval的开始时间，返回次日的日期  
新增：当前时间为19:00，处于interval的时间范围内，返回当日的日期


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 a4ad712f..e68bc4d5 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
@@ -130,8 +130,7 @@ trait Rules extends DimRules {
         // "一日"单独是latent，但是可以参与组合
         and(isDimension(Time), or(and(or(isNotLatent, isLatent0oClockOfDay), not(isAPartOfDay)), isADayOfMonth)).predicate
       ),
-      prod = tokens {
-        case (t1@Token(Time, td1: TimeData)) :: (t2@Token(Time, td2: TimeData)) :: _
+      prod = { case (options, (t1@Token(Time, td1: TimeData)) :: (t2@Token(Time, td2: TimeData)) :: _)
           if td1.timeGrain > td2.timeGrain && !(td1.hint == Hint.Date && td2.hint == Hint.Date) ||
             // 上午的8-9点
             td1.timeGrain == td2.timeGrain && td1.timeGrain == Hour && isAPartOfDay(t1) && !isAPartOfDay(t2) =>
@@ -148,7 +147,7 @@ trait Rules extends DimRules {
             // 10号八点，需要去掉AMPM (今天是10号9点时，不应再出20点)
             // 今天8点，需要根据当前时间是出8/20点
             val _td2 = if (td1.hint == Hint.RecentNominal) td2 else removeAMPM(td2)
-            val _td1 = FuzzyDayIntervals.enlarge(td1)
+            val _td1 = FuzzyDayIntervals.enlarge(td1, options.timeOptions.beforeEndOfInterval)
             val td = intersect(_td1, _td2).map(_.copy(hint = hint))
             tt(td)
           }
@@ -159,8 +158,8 @@ trait Rules extends DimRules {
     name = "intersect: <x> 的 <y>",
     // "一日"单独是latent，但是可以参与组合
     pattern = List(isNotLatent.predicate, "的".regex, or(or(isNotLatent, isLatent0oClockOfDay), isADayOfMonth).predicate),
-    prod = tokens {
-      case (t1@Token(Time, td1: TimeData)) :: _ :: (t2@Token(Time, td2: TimeData)) :: _
+    prod = {
+      case (options, (t1@Token(Time, td1: TimeData)) :: _ :: (t2@Token(Time, td2: TimeData)) :: _)
         if td1.timeGrain > td2.timeGrain ||
           // 上午的8-9点
           td1.timeGrain == td2.timeGrain && td1.timeGrain == Hour && isAPartOfDay(t1) && !isAPartOfDay(t2) =>
@@ -169,7 +168,7 @@ trait Rules extends DimRules {
           val hint =
             if (td1.timeGrain == Year && td2.hint == MonthOnly) YearMonth
             else NoHint
-          val _td1 = FuzzyDayIntervals.enlarge(td1)
+          val _td1 = FuzzyDayIntervals.enlarge(td1, options.timeOptions.beforeEndOfInterval)
           val td = intersect(_td1, removeAMPM(td2)).map(_.copy(hint = hint))
           tt(td)
         }
@@ -406,9 +405,9 @@ trait Rules extends DimRules {
   val ruleFromToInterval = Rule(
     name = "<from> 到 <to>",
     pattern = List(isNotLatent.predicate, "(至|到|~)".regex, isNotLatent.predicate),
-    prod = tokens {
-      case Token(Time, td1 @ TimeData(pred1, _, g1, _, _, _, _, _, _, _, _, _)) :: _ ::
-            Token(Time, td2 @ TimeData(pred2, _, g2, _, _, _, _, _, _, _, _, _)) :: _ =>
+    prod = {
+      case (options, Token(Time, td1 @ TimeData(pred1, _, g1, _, _, _, _, _, _, _, _, _)) :: _ ::
+            Token(Time, td2 @ TimeData(pred2, _, g2, _, _, _, _, _, _, _, _, _)) :: _) =>
         // 限定求交的Grain，避免日交月
         val m1 = maxPredicateGrain(pred1)
         val m2 = maxPredicateGrain(pred2)
@@ -423,7 +422,7 @@ trait Rules extends DimRules {
               case _                  => Open
             }
           } else Open
-          tt(interval(intervalType, td1, td2).map(_.copy(hint = FinalRule)))
+          tt(interval(intervalType, td1, td2, options.timeOptions.beforeEndOfInterval).map(_.copy(hint = FinalRule)))
         } else None
     }
   )
@@ -434,9 +433,9 @@ trait Rules extends DimRules {
   val ruleFromToIntervalAbbr1 = Rule(
     name = "<from>(no grain) 到 <to>",
     pattern = List(isNatural.predicate, "(至|到|~)".regex, and(isNotLatent, or(xGrain(Month), xGrain(Day), xGrain(Hour))).predicate),
-    prod = tokens {
-      case Token(_, vd: NumeralData) :: _ ::
-        Token(Time, td2@TimeData(pred2: TimeDatePredicate, _, g2, _, _, _, _, _, _, _, _, _)) :: _ =>
+    prod = {
+      case (options, Token(_, vd: NumeralData) :: _ ::
+        Token(Time, td2@TimeData(pred2: TimeDatePredicate, _, g2, _, _, _, _, _, _, _, _, _)) :: _ )=>
         val _from = vd.value.toInt
         val td1: Option[TimeData] = g2 match {
           case Month if 0 <= _from && _from <= 12 => td2.copy(timePred = pred2.copy(month = _from))
@@ -445,7 +444,7 @@ trait Rules extends DimRules {
           case _ => None
         }
         td1 match {
-          case Some(x) => tt(interval(if (g2 != Hour) Closed else Open, x, td2))
+          case Some(x) => tt(interval(if (g2 != Hour) Closed else Open, x, td2, options.timeOptions.beforeEndOfInterval))
           case None => None
         }
     }
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/Time.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/Time.scala
index 2a43cf2e..b0a70fc2 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/Time.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/Time.scala
@@ -16,7 +16,7 @@
 
 package com.xiaomi.duckling.dimension.time
 
-import scala.beans.BooleanBeanProperty
+import scala.beans.{BeanProperty, BooleanBeanProperty}
 
 import com.typesafe.scalalogging.LazyLogging
 
@@ -99,7 +99,11 @@ class TimeOptions {
   /**
    * duration: 模糊的数量
    */
-  @BooleanBeanProperty var durationFuzzyValue: Int = 3
+  @BeanProperty var durationFuzzyValue: Int = 3
+  /**
+   * 参考时间点 ref 位于 [a, b) 之间时，false 取 a >= ref 的值，true取 a <= ref < b 的
+   */
+  @BooleanBeanProperty var beforeEndOfInterval: Boolean = false
 }
 
 case class TimeData(timePred: TimePredicate,
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/date/Rules.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/date/Rules.scala
index 7b5c03f3..124ed407 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/date/Rules.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/date/Rules.scala
@@ -17,19 +17,19 @@
 package com.xiaomi.duckling.dimension.time.date
 
 import scalaz.std.string.parseInt
-
 import java.time.LocalDate
+
 import scala.util.Try
 
 import com.xiaomi.duckling.Types._
 import com.xiaomi.duckling.dimension.DimRules
 import com.xiaomi.duckling.dimension.implicits._
-import com.xiaomi.duckling.dimension.matcher.GroupMatch
+import com.xiaomi.duckling.dimension.matcher.{GroupMatch, RegexMatch}
 import com.xiaomi.duckling.dimension.matcher.Prods.{regexMatch, singleRegexMatch}
 import com.xiaomi.duckling.dimension.numeral.Predicates.{getIntValue, isIntegerBetween}
 import com.xiaomi.duckling.dimension.numeral.seq.{DigitSequence, DigitSequenceData}
 import com.xiaomi.duckling.dimension.time.Prods.intersectDOM
-import com.xiaomi.duckling.dimension.time.duration.{Duration, DurationData, isADecade}
+import com.xiaomi.duckling.dimension.time.duration.{isADecade, Duration, DurationData}
 import com.xiaomi.duckling.dimension.time.enums.Grain._
 import com.xiaomi.duckling.dimension.time.enums.Hint.{NoHint, RecentNominal, YearMonth}
 import com.xiaomi.duckling.dimension.time.enums.IntervalType.{Closed, Open}
@@ -257,32 +257,33 @@ trait Rules extends DimRules {
   val ruleDecade = Rule(
     name = "date: tens of decade",
     pattern = List(isADecade.predicate, "年代".regex),
-    prod = tokens {
-      case t1 :: _ =>
+    prod = {
+      case (options, t1 :: _) =>
         val y = getIntValue(t1).get.toInt
         val adjustY = if (y > 20) 1900 + y else y + 2000
         val from = year(adjustY)
         val to = year(adjustY + 10)
-        for (td <- interval(Open, from, to)) yield Token(Date, td)
+        for (td <- interval(Open, from, to, options.timeOptions.beforeEndOfInterval)) yield Token(Date, td)
     }
   )
 
   val ruleQuarter = Rule(
     name = "date - <ordinal> <quarter>",
     pattern = List("第".regex, isAQuarterOfYear.predicate),
-    prod = tokens {
-      case _ :: Token(Duration, DurationData(value, _, _, _, _)) :: _ =>
-        for (td <- interval(Closed, month(3 * value - 2), month(3 * value))) yield {
+    prod = {
+      case (options, _ :: Token(Duration, DurationData(value, _, _, _, _)) :: _) =>
+        for (td <- interval(Closed, month(3 * value - 2), month(3 * value), options.timeOptions.beforeEndOfInterval)) yield {
           Token(Date, td.copy(reset = (Grain.resetTo(Quarter), 0)))
         }
     }
   )
 
-  val ruleSpecial0 = Rule(name = "date - special days: 今明两天", pattern = List("今明两天|今天明天".regex), prod = tokens {
-    case _ =>
-      val from = cycleNth(Day, 0, Day)
-      val to = cycleNth(Day, 1, Day)
-      for (td <- interval(Open, from, to)) yield Token(Date, td)
+  val ruleSpecial0 = Rule(name = "date - special days: 今明两天", pattern = List("今明两天|今天明天".regex),
+    prod = {
+      case (options, _) =>
+        val from = cycleNth(Day, 0, Day)
+        val to = cycleNth(Day, 1, Day)
+        for (td <- interval(Open, from, to, options.timeOptions.beforeEndOfInterval)) yield Token(Date, td)
   })
 
   val ruleSpecial1 = Rule(name = "date - special days: 元月", pattern = List("元月份?".regex), prod = tokens {
@@ -292,28 +293,30 @@ trait Rules extends DimRules {
   val ruleSpecial2 = Rule(
     name = "date - special days: 最近",
     pattern = List("(最近|近期|这段时间)".regex),
-    prod = tokens {
-      case _ =>
+    prod = {
+      case (options, _) =>
         val from = cycleNth(Day, 0, Day)
         val to = cycleNth(Day, 2, Day)
-        for (td <- interval(Open, from, to)) yield {
+        for (td <- interval(Open, from, to, options.timeOptions.beforeEndOfInterval)) yield {
           Token(Date, td.copy(hint = Hint.UncertainRecent))
         }
     }
   )
 
-  val ruleSpecial3 = Rule(name = "date - special days: 明后天", pattern = List("明后两?天|明天后天".regex), prod = tokens {
-    case _ =>
-      val from = cycleNth(Day, 1, Day)
-      val to = cycleNth(Day, 2, Day)
-      for (td <- interval(Open, from, to)) yield Token(Date, td)
+  val ruleSpecial3 = Rule(name = "date - special days: 明后天", pattern = List("明后两?天|明天后天".regex),
+    prod = {
+      case (options, _) =>
+        val from = cycleNth(Day, 1, Day)
+        val to = cycleNth(Day, 2, Day)
+        for (td <- interval(Open, from, to, options.timeOptions.beforeEndOfInterval)) yield Token(Date, td)
   })
 
-  val ruleSpecial4 = Rule(name = "date - special days: 今明后", pattern = List("今明后(三?天)?|今天明天后天(三天)?".regex), prod = tokens {
-    case _ =>
-      val from = cycleNth(Day, 0, Day)
-      val to = cycleNth(Day, 2, Day)
-      for (td <- interval(Open, from, to)) yield Token(Date, td)
+  val ruleSpecial4 = Rule(name = "date - special days: 今明后", pattern = List("今明后(三?天)?|今天明天后天(三天)?".regex),
+    prod = {
+      case (options, _) =>
+        val from = cycleNth(Day, 0, Day)
+        val to = cycleNth(Day, 2, Day)
+        for (td <- interval(Open, from, to, options.timeOptions.beforeEndOfInterval)) yield Token(Date, td)
   })
 
   val ruleEndOfMonth = Rule(name = "date - 月底", pattern = List("月(底|末)".regex), prod = tokens {
@@ -323,14 +326,15 @@ trait Rules extends DimRules {
       Token(Date, td)
   })
 
-  val ruleHalfYear = Rule(name = "date - 上/下半年", pattern = List("(上|下)半年".regex), prod = regexMatch {
-    case _ :: half :: _ =>
-      val start = if (half == "上") 0 else 6
-      val from = cycleNth(Month, start, Year)
-      val to = cycleNth(Month, start + 6, Year)
-      for (td <- interval(Open, from, to)) yield {
-        Token(Date, td)
-      }
+  val ruleHalfYear = Rule(name = "date - 上/下半年", pattern = List("(上|下)半年".regex),
+    prod = {
+      case (options: Options, Token(RegexMatch, GroupMatch(_ :: half :: _)) :: _) =>
+        val start = if (half == "上") 0 else 6
+        val from = cycleNth(Month, start, Year)
+        val to = cycleNth(Month, start + 6, Year)
+        for (td <- interval(Open, from, to, options.timeOptions.beforeEndOfInterval)) yield {
+          Token(Date, td)
+        }
   })
 
   val ruleIntersect =
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 cb63082d..ce43a8ad 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
@@ -103,7 +103,7 @@ object TimeDataHelpers {
     year(n).copy(latent = true)
   }
 
-  def weekend: TimeData = form(Weekend, interval1(Open, dayOfWeek(6), dayOfWeek(7)))
+  def weekend(beforeEndOfInterval: Boolean): TimeData = form(Weekend, interval1(Open, dayOfWeek(6), dayOfWeek(7), beforeEndOfInterval))
 
   def timeOfDayAMPM(isAM: Boolean, td: TimeData): TimeData = {
     val ampmPred: TimeDatePredicate = TimeDatePredicate(ampm = if (isAM) AM else PM)
@@ -224,20 +224,20 @@ object TimeDataHelpers {
     predNthAfter(nth - 1, dow_, month_)
   }
 
-  def interval(intervalType: IntervalType, td1: TimeData, td2: TimeData): Option[TimeData] = {
-    interval1(intervalType, td1, td2) match {
+  def interval(intervalType: IntervalType, td1: TimeData, td2: TimeData, beforeEndOfInterval: Boolean): Option[TimeData] = {
+    interval1(intervalType, td1, td2, beforeEndOfInterval) match {
       case td: TimeData if td.timePred == EmptyTimePredicate => None
       case res                                               => Some(res)
     }
   }
 
-  def interval1(intervalType: IntervalType, td1: TimeData, td2: TimeData): TimeData = {
+  def interval1(intervalType: IntervalType, td1: TimeData, td2: TimeData, beforeEndOfInterval: Boolean): TimeData = {
     val it =
       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),
+      timePred = mkTimeIntervalsPredicate(it, td1, td2, beforeEndOfInterval),
       timeGrain = if (td1.timeGrain > td2.timeGrain) td2.timeGrain else td1.timeGrain
     )
   }
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 28cacfdb..d08fd6f7 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
@@ -30,23 +30,30 @@ import com.xiaomi.duckling.dimension.time.enums._
 
 object TimeObjectHelpers {
 
-  def timeRound(t: TimeObject, grain: Grain): TimeObject = {
-    val zone = t.start.zone
-    val d = t.start
+  def timeRound0(dt: DuckDateTime, grain: Grain, calendar: Option[Calendar] = None): (DuckDateTime, Grain) = {
+    val zone = dt.zone
     val (year, month, dayOfMonth, hours, mins, secs) =
-      (d.year, d.month, d.dayOfMonth, d.hour, d.minute, d.second)
+      (dt.year, dt.month, dt.dayOfMonth, dt.hour, dt.minute, dt.second)
     val newMonth = if (grain > Month) 1 else month
     val newDayOfMonth = if (grain > Week) 1 else dayOfMonth
     val newHours = if (grain > Hour) 0 else hours
     val newMins = if (grain > Minute) 0 else mins
     val newSecs = if (grain > Second) 0 else secs
     val time = LocalTime.of(newHours, newMins, newSecs)
-    val date = t.start.date.of(year, newMonth, newDayOfMonth)
+    val date = (calendar match {
+      case Some(Solar) => dt.date.toSolar
+      case Some(Lunar(leap)) => dt.date.toLunar
+      case _ => dt.date
+    }).of(year, newMonth, newDayOfMonth)
     val datetime = DuckDateTime(date, time, zone)
-    val (start, g) =
-      if (grain == Week) (datetime.minusDays(datetime.dayOfWeek - 1), Week)
-      else (datetime, grain)
-    TimeObject(start = start, grain = g)
+    if (grain == Week) (datetime.minusDays(datetime.dayOfWeek - 1), Week)
+    else (datetime, grain)
+  }
+
+  def timeRound(t: TimeObject, grain: Grain, calendar: Option[Calendar] = None): TimeObject = {
+    val (start, g) = timeRound0(t.start, grain, calendar)
+    val end = t.end.map(timeRound0(_, grain, calendar)).map(_._1)
+    TimeObject(start = start, grain = g, end = end)
   }
 
   def timeSequence(grain: Grain,
@@ -148,6 +155,14 @@ object TimeObjectHelpers {
     else timeBefore(timeRound(t1, grain), timeRound(t2, grain))
   }
 
+  @tailrec
+  def endBefore(t1: TimeObject, t2: TimeObject, grain: Grain = NoGrain): Boolean = {
+    if (grain == NoGrain) {
+      if (t1.end.nonEmpty) t1.end.get.isBefore(t2.start)
+      else timeBefore(t1, t2, grain)
+    } else endBefore(timeRound(t1, grain), timeRound(t2, grain))
+  }
+
   def timeInterval(intervalType: IntervalType, to1: TimeObject, to2: TimeObject): TimeObject = {
     val TimeObject(s1, g1, _, _) = to1
     val TimeObject(s2, g2, e2, _) = to2
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/helper/TimePredicateHelpers.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/helper/TimePredicateHelpers.scala
index 81e6c4fe..10eb831d 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/helper/TimePredicateHelpers.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/helper/TimePredicateHelpers.scala
@@ -90,7 +90,8 @@ object TimePredicateHelpers {
 
   def mkTimeIntervalsPredicate(t: IntervalType,
                                ta: TimeData,
-                               tb: TimeData): TimePredicate = {
+                               tb: TimeData,
+                               beforeEndOfInterval: Boolean): TimePredicate = {
     val a = ta.timePred
     val b = tb.timePred
 
@@ -102,7 +103,7 @@ object TimePredicateHelpers {
       EmptyTimePredicate
     }
     // this is potentially quadratic, but the sizes involved should be small
-    else TimeIntervalsPredicate(t, a, b)
+    else TimeIntervalsPredicate(t, a, b, beforeEndOfInterval)
   }
 
   def isPartOfDay(td: TimeData): Boolean = {
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 3f0d5e26..b0aaf5ba 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
@@ -113,12 +113,16 @@ package object time {
           // 逻辑比较混乱，待收集到问题再处理
           val happened =
             td.timePred match {
-              case _: TimeDatePredicate | _: TimeIntervalsPredicate | _: IntersectTimePredicate =>
+              case _: TimeDatePredicate | _: IntersectTimePredicate =>
                 // 若参考时间是2013/2/12 04:30，在alwaysInFuture情况下
                 // 1. 过了一部分还需要再出的，12号 => 2/12，2月 => 2013/2
                 // 2. 问4点，需要给出 16:00
                 val g = if (td.timeGrain >= Grain.Day) td.timeGrain else Grain.NoGrain
                 timeBefore(ahead, refTime, g)
+              case TimeIntervalsPredicate(_, _, _, beforeEndOfInterval) =>
+                val g = if (td.timeGrain >= Grain.Day) td.timeGrain else Grain.NoGrain
+                if (!beforeEndOfInterval) timeBefore(ahead, refTime, g)
+                else endBefore(ahead, refTime, g)
               case _ => false
             }
           if (happened || td.notImmediate && timeIntersect(ahead)(refTime).nonEmpty) {
@@ -145,7 +149,7 @@ package object time {
             hour.map(runHourPredicate(ampm)),
             dayOfWeek.map(runDayOfTheWeekPredicate),
             dayOfMonth.map(runDayOfTheMonthPredicate),
-            month.map(runMonthPredicate(calendar)),
+            month.map(runMonthPredicate(calendar.orElse(Some(Solar)))),
             year.map(runYearPredicate)
           ).flatten
 
@@ -158,8 +162,8 @@ package object time {
           series
         }
       case IntersectTimePredicate(pred1, pred2) => runIntersectPredicate(pred1, pred2)
-      case TimeIntervalsPredicate(intervalType, p1, p2) =>
-        runTimeIntervalsPredicate(intervalType, p1, p2)
+      case TimeIntervalsPredicate(intervalType, p1, p2, beforeEndOfInterval) =>
+        runTimeIntervalsPredicate(intervalType, p1, p2, beforeEndOfInterval)
       case TimeOpenIntervalPredicate(t) => runTimeOpenIntervalPredicate(t)
       case SequencePredicate(xs)               => runSequencePredicate(xs)
       case ReplacePartPredicate(td1, td2)      => runReplacePartPredicate(td1, td2)
@@ -289,7 +293,7 @@ package object time {
   }
 
   def runMonthPredicate(calendar: Option[Calendar])(n: Int)(t: TimeObject, context: TimeContext): PastFutureTime = {
-    val y = timeRound(t, Year)
+    val y = timeRound(t, Year, calendar)
     val rounded =
       calendar match {
         case Some(Solar) | None => timePlus(y, Month, n - 1)
@@ -338,7 +342,8 @@ package object time {
 
   def runTimeIntervalsPredicate(intervalType: IntervalType,
                                 pred1: TimePredicate,
-                                pred2: TimePredicate): SeriesPredicateF = {
+                                pred2: TimePredicate,
+                                beforeEndOfInterval: Boolean): SeriesPredicateF = {
     // Pick the first interval *after* the given time segment
     def f(thisSegment: TimeObject, ctx: TimeContext): Option[TimeObject] = {
       runPredicate(pred2)(thisSegment, ctx) match {
@@ -350,7 +355,17 @@ package object time {
       }
     }
 
-    timeSeqMap(dontReverse = true, f, pred1)
+    def b(thisSegment: TimeObject, ctx: TimeContext): Option[TimeObject] = {
+      runPredicate(pred1)(thisSegment, ctx) match {
+        case (past, future) =>
+          val choosed = future.take(safeMax).find(t => timeStartsBeforeTheEndOf(t)(thisSegment))
+            .orElse(past.take(safeMax).find(t => timeStartsBeforeTheEndOf(t)(thisSegment)))
+          choosed.map(timeInterval(intervalType, _, thisSegment))
+        case _ => None
+      }
+    }
+    if (!beforeEndOfInterval) timeSeqMap(dontReverse = true, f, pred1)
+    else timeSeqMap(dontReverse = true, b, pred2)
   }
 
   /**
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/predicates.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/predicates.scala
index 6ed6de70..44e35f53 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/predicates.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/predicates.scala
@@ -87,7 +87,7 @@ object predicates {
     }
   }
 
-  case class TimeIntervalsPredicate(t: IntervalType, p1: TimePredicate, p2: TimePredicate)
+  case class TimeIntervalsPredicate(t: IntervalType, p1: TimePredicate, p2: TimePredicate, beforeEndOfInterval: Boolean)
     extends TimePredicate {
 
     override val maxGrain: Option[Grain] = {
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/FuzzyDayIntervals.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/FuzzyDayIntervals.scala
index 8ba188cc..46154649 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/FuzzyDayIntervals.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/FuzzyDayIntervals.scala
@@ -18,7 +18,7 @@ package com.xiaomi.duckling.dimension.time.rule
 
 import com.xiaomi.duckling.Types._
 import com.xiaomi.duckling.dimension.implicits._
-import com.xiaomi.duckling.dimension.matcher.GroupMatch
+import com.xiaomi.duckling.dimension.matcher.{GroupMatch, RegexMatch}
 import com.xiaomi.duckling.dimension.matcher.Prods._
 import com.xiaomi.duckling.dimension.time.{Time, TimeData}
 import com.xiaomi.duckling.dimension.time.Prods._
@@ -51,11 +51,11 @@ object FuzzyDayIntervals {
     }
   }
 
-  def ofInterval(s: String, larger: Boolean = false): Option[(String, TimeData)] = {
+  def ofInterval(s: String, larger: Boolean = false, beforeEndOfInterval: Boolean): Option[(String, TimeData)] = {
     val (name, (from, to), (_from, _to)) = between(s)
     val td =
-      if (larger) interval(Open, hour(is12H = false, _from), hour(is12H = false, _to))
-      else interval(Open, hour(is12H = false, from), hour(is12H = false, to))
+      if (larger) interval(Open, hour(is12H = false, _from), hour(is12H = false, _to), beforeEndOfInterval)
+      else interval(Open, hour(is12H = false, from), hour(is12H = false, to), beforeEndOfInterval)
     td.map((name, _))
   }
 
@@ -64,49 +64,57 @@ object FuzzyDayIntervals {
    * @param td
    * @return
    */
-  def enlarge(td: TimeData): TimeData = {
+  def enlarge(td: TimeData, beforeEndOfInterval: Boolean): TimeData = {
     (td.form, td.timePred) match {
       case (Some(PartOfDay(part)), _: TimeIntervalsPredicate) =>
-        val _td = ofInterval(part, larger = true).map(_._2).get
+        val _td = ofInterval(part, larger = true, beforeEndOfInterval).map(_._2).get
         _td.copy(latent = td.latent)
       case _ => td
     }
   }
 
   val ruleFuzzyDayIntervals =
-    Rule(name = "fuzzy day intervals", pattern = List(pattern.regex), prod = singleRegexMatch {
-      case s =>
-        for ((name, td) <- ofInterval(s)) yield {
-          tt(partOfDay(name, td).copy(latent = s.length == 1))
-        }
-    })
+    Rule(name = "fuzzy day intervals", pattern = List(pattern.regex),
+      prod = {
+        case (options, tokens: List[Token]) =>
+          tokens.headOption.flatMap {
+            case Token(RegexMatch, GroupMatch(s :: _)) =>
+              for ((name, td) <- ofInterval(s, beforeEndOfInterval = options.timeOptions.beforeEndOfInterval)) yield {
+                tt(partOfDay(name, td).copy(latent = s.length == 1))
+              }
+            case _ => None
+          }
+      })
 
   val ruleRecent = Rule(
     name = "recent tonight/morning omit unit",
     pattern = List("(今|明|昨)(早上?|中午|晚上?)".regex),
-    prod = regexMatch {
-      case _ :: s0 :: s1 :: _ =>
-        val offset = s0 match {
-          case "今" => 0
-          case "明" => 1
-          case "昨" => -1
-        }
-        val part = s1(0) match {
-          case '早' => "早上"
-          case '中' => "中午"
-          case '晚' => "晚上"
-        }
-        val tdInterval = ofInterval(part)
-        for {
-          (_, td) <- tdInterval
-          td1 <- intersect(cycleNth(Day, offset), td)
-        } yield {
-          tt(partOfDay(part, td1))
+    prod = {
+      case (options, tokens: List[Token]) =>
+        tokens.headOption.flatMap {
+          case Token(RegexMatch, GroupMatch(_ :: s0 :: s1 :: _)) =>
+            val offset = s0 match {
+              case "今" => 0
+              case "明" => 1
+              case "昨" => -1
+            }
+            val part = s1(0) match {
+              case '早' => "早上"
+              case '中' => "中午"
+              case '晚' => "晚上"
+            }
+            val tdInterval = ofInterval(part, beforeEndOfInterval = options.timeOptions.beforeEndOfInterval)
+            for {
+              (_, td) <- tdInterval
+              td1 <- intersect(cycleNth(Day, offset), td)
+            } yield {
+              tt(partOfDay(part, td1))
+            }
         }
     }
   )
 
-  private def fuzzyIntervalTimeOfDay(td0: TimeData, td: TimeData): Option[Token] = {
+  private def fuzzyIntervalTimeOfDay(td0: TimeData, td: TimeData, beforeEndOfInterval: Boolean): Option[Token] = {
     val TimeData(pred, _, _, _, Some(form), _, _, _, _, _, _, _) = td
     val part = td0.form.get.asInstanceOf[PartOfDay].part
     // 利用左侧的区间将右侧的时间从12AMPM转换到24H制
@@ -117,13 +125,13 @@ object FuzzyDayIntervals {
           (is12H, hAdjust) <- hAdjustP.hour
         } yield timeOfDay(hAdjust, is12H = false, td).copy(timePred = hAdjustP, hint = Hint.NoHint)
       case (
-          TimeIntervalsPredicate(t, p1: TimeDatePredicate, p2: TimeDatePredicate),
+          TimeIntervalsPredicate(t, p1: TimeDatePredicate, p2: TimeDatePredicate, beforeEndOfInterval),
           IntervalOfDay
           ) =>
         for {
           from <- updatePredicateByFuzzyInterval(part, p1)
           to <- updatePredicateByFuzzyInterval(part, p2)
-        } yield td.copy(timePred = TimeIntervalsPredicate(t, from, to))
+        } yield td.copy(timePred = TimeIntervalsPredicate(t, from, to, beforeEndOfInterval))
       case _ => None
     }
     // 组装，上午8点，以及[昨晚]8点
@@ -143,9 +151,9 @@ object FuzzyDayIntervals {
       and(isAPartOfDay, not(isHint(PartOfDayAtLast))).predicate,
       and(or(isNotLatent, isLatent0oClockOfDay), or(isATimeOfDay, isIntervalOfDay)).predicate
     ),
-    prod = tokens {
-      case Token(Time, td0: TimeData) :: Token(Time, td: TimeData) :: _ =>
-        fuzzyIntervalTimeOfDay(td0, td)
+    prod = {
+      case (options: Options, Token(Time, td0: TimeData) :: Token(Time, td: TimeData) :: _) =>
+        fuzzyIntervalTimeOfDay(td0, td, options.timeOptions.beforeEndOfInterval)
     }
   )
 
@@ -157,9 +165,9 @@ object FuzzyDayIntervals {
       "的".regex,
       and(or(isNotLatent, isLatent0oClockOfDay), or(isATimeOfDay, isIntervalOfDay)).predicate
     ),
-    prod = tokens {
-      case Token(Time, td0: TimeData) :: _ :: Token(Time, td: TimeData) :: _ =>
-        fuzzyIntervalTimeOfDay(td0, td)
+    prod = {
+      case (options, Token(Time, td0: TimeData) :: _ :: Token(Time, td: TimeData) :: _) =>
+        fuzzyIntervalTimeOfDay(td0, td, options.timeOptions.beforeEndOfInterval)
     }
   )
 
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 a2196a01..8121bb88 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
@@ -77,7 +77,7 @@ object SolarTerms {
           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, hint = Hint.Season))
+          for (td <- interval(Open, from, to, options.timeOptions.beforeEndOfInterval)) yield Token(Date, td.copy(holiday = holiday, hint = Hint.Season))
         } else None
     })
 
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/Weeks.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/Weeks.scala
index 34dde135..26b26a18 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/Weeks.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/dimension/time/rule/Weeks.scala
@@ -70,8 +70,8 @@ object Weeks {
     }
   )
 
-  val ruleWeekend = Rule(name = "week-end", pattern = List("周末".regex), prod = tokens {
-    case _ => tt(weekend)
+  val ruleWeekend = Rule(name = "week-end", pattern = List("周末".regex), prod = {
+    case (options, _) => tt(weekend(options.timeOptions.beforeEndOfInterval))
   })
 
   def mkRuleDaysOfWeek(pairs: List[(String, String)]): List[Rule] = {
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/ranking/Testing.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/ranking/Testing.scala
index 7630181c..8ce13d8b 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/ranking/Testing.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/ranking/Testing.scala
@@ -10,6 +10,7 @@ object Testing {
   val testContext: Context =
     Context(
       locale = Locale.CHINA,
+      // 星期二
       referenceTime = ZonedDateTime.of(LocalDateTime.of(2013, 2, 12, 4, 30, 0), ZoneCN)
     )
 
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 1afcdc11..4b667c6f 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
@@ -76,7 +76,8 @@ object NaiveBayesConsole extends LazyLogging {
         "inherit-duration-grain",
         "seasons",
         "sequence",
-        "fuzzy-on"
+        "fuzzy-on",
+        "before-end-of-interval"
       ),
       NullCompleter.INSTANCE
     )
@@ -125,6 +126,7 @@ object NaiveBayesConsole extends LazyLogging {
               case "seasons" => options.timeOptions.setParseFourSeasons(value); options
               case "sequence" => options.timeOptions.setSequence(value); options
               case "fuzzy-on" => options.timeOptions.setDurationFuzzyOn(value); options
+              case "before-end-of-interval" => options.timeOptions.setBeforeEndOfInterval(value); options
               case _ => options
             }
             opt
diff --git a/duckling-fork-chinese/learning/src/main/scala/com/xiaomi/duckling/task/NaiveBayesDebug.scala b/duckling-fork-chinese/learning/src/main/scala/com/xiaomi/duckling/task/NaiveBayesDebug.scala
index 08ddc91c..67414bfa 100644
--- a/duckling-fork-chinese/learning/src/main/scala/com/xiaomi/duckling/task/NaiveBayesDebug.scala
+++ b/duckling-fork-chinese/learning/src/main/scala/com/xiaomi/duckling/task/NaiveBayesDebug.scala
@@ -16,8 +16,6 @@
 
 package com.xiaomi.duckling.task
 
-import java.time.ZonedDateTime
-
 import org.apache.commons.lang3.time.StopWatch
 import org.json4s.jackson.Serialization.write
 
@@ -57,6 +55,7 @@ object NaiveBayesDebug {
     options.timeOptions.setResetTimeOfDay(false)
     options.timeOptions.setRecentInFuture(true)
     options.timeOptions.setAlwaysInFuture(true)
+    options.timeOptions.setBeforeEndOfInterval(true)
     options.numeralOptions.setAllowZeroLeadingDigits(false)
     options.numeralOptions.setCnSequenceAsNumber(false)
 


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 2b4df093..856ff7f3 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
@@ -20,7 +20,7 @@ import java.time.{LocalDateTime, ZonedDateTime}
 
 import com.xiaomi.duckling.Api.analyze
 import com.xiaomi.duckling.Types._
-import com.xiaomi.duckling.dimension.time.Types.{InstantValue, SimpleValue}
+import com.xiaomi.duckling.dimension.time.Types.{InstantValue, SimpleValue, IntervalValue}
 import com.xiaomi.duckling.dimension.time.enums.Grain.Year
 import com.xiaomi.duckling.dimension.time.helper.TimeDataHelpers._
 import com.xiaomi.duckling.dimension.time.predicates.ReplacePartPredicate
@@ -113,6 +113,32 @@ class TimeTest extends UnitSpec {
       }
     }
 
+    it("beforeEndOfInterval") {
+      val _options = options.copy(timeOptions = new TimeOptions)
+      _options.timeOptions.setBeforeEndOfInterval(true)
+
+      val cases = Table(("query", "start", "end")
+        , ("三点到五点", "12 03:00:00", "12 05:00:00")
+        , ("星期一到星期三", "2013-02-11", "2013-02-14")
+        , ("1月到三月", "2013-01-01", "2013-04-01")
+        , ("凌晨", "12 00:00:00", "12 06:00:00")
+        , ("过年到现在", "农历 二〇一三年正月初一", "2013-02-12")
+        , ("一月八号到过年", "2014-01-08", "农历 二〇一四年正月初二")
+        , ("明年一月五号到二月二十号", "2014-01-05", "2014-02-21")
+      )
+
+      forAll(cases) { (query, s, t) =>
+        val answers = parse(query, options = _options)
+        answers should not be empty
+        answers.head.token.value match {
+          case TimeValue(IntervalValue(InstantValue(start, _), InstantValue(end, _)), _, _, _, _, _) =>
+            start.toString should include(s)
+            end.toString should include(t)
+          case _ => "parsing failure"
+        }
+      }
+    }
+
     it("fix #187") {
       val opt = options.copy(full = true, withLatent = false)
       val cases = Table("query", "国庆晚", "十月一晚", "下钟", "晚")