Repository: XiaoMi/MiNLP (language: python)

Issue:
[DUCK] 在某些情况 "duration + 后" 需要带有粒度信息
- 两个小时后，闹钟需要秒级/分钟级，但是天气其实是需要小时级
- 三天后，天气需要的是天级


Discussion / hints:

Fix patch:
diff --git a/duckling-fork-chinese/README.md b/duckling-fork-chinese/README.md
index b44b512d..52694b8a 100644
--- a/duckling-fork-chinese/README.md
+++ b/duckling-fork-chinese/README.md
@@ -19,18 +19,18 @@
 
 ## 1.1 Scala
 
-目前只发布了scala_2.11版本的包，有需要2.12/2.13的，可以在Issue区留言。
+目前只发布了scala_2.11/2.12/2.13的版本，3.0尚未适配，有需要可以在Issue区留言。
 ```
 maven
 
 <dependency>
   <groupId>com.xiaomi.duckling</groupId>
-  <artifactId>duckling-core_2.11</artifactId>
-  <version>1.2.2D</version>
+  <artifactId>duckling-core_2.1x</artifactId>
+  <version>x.y.z</version>
 </dependency>
 
 sbt
-"com.xiaomi.duckling" % "duckling-core" %% "1.2.2D"
+"com.xiaomi.duckling" % "duckling-core" %% "x.y.z"
 ```
 
 
@@ -120,6 +120,24 @@ cd server/target/universal/stage
 bash bin/duckling-server
 ```
 
+### 1.4 Console
+
+借助于sbt/console可以在命令行中进行尝试，例如
+```bash
+> sbt core/console
+> com.xiaomi.duckling.task.NaiveBayesConsole.run()
+
+# 设置解析维度，可以按tab进行自动补全（下面都可以补全）
+> dimension time duration
+> 三天后
+# -2.76096 => {"timeValue":{...,"grain":"Second"}}...}
+
+
+# 设置参数，开放了四个，有需要可以增加
+> option inherit-duration-grain true
+> 三天后
+# -2.76096 => {"timeValue":{...,"grain":"Day"}}...}
+```
 
 
 在线体验Demo：`/duckling?dim=...&query=...`
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/Types.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/Types.scala
index cb6d63a5..0f71be43 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/Types.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/Types.scala
@@ -39,6 +39,7 @@ object Types {
   type Pattern = List[PatternItem]
   type Production = PartialFunction[(Options, List[Token]), Option[Token]]
   type TokensProduction = PartialFunction[List[Token], Option[Token]]
+  type OptionTokensProduction = PartialFunction[(Options, List[Token]), Option[Token]]
   type Predicate = PartialFunction[Token, Boolean]
   type Extraction = PartialFunction[(Document, List[Node]), List[Feature]]
 
@@ -53,6 +54,11 @@ object Types {
     case _ => None
   }
 
+  def optTokens(tp: OptionTokensProduction): Production = {
+    case (option: Options, _tokens: List[Token]) if tp.isDefinedAt((option, _tokens)) => tp(option, _tokens)
+    case _ => None
+  }
+
   // Predicate helpers
   def isDimension(dim: Dimension*): Predicate = {
     case token: Token => dim.contains(token.dim)
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 775e995a..6f404c9d 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
@@ -261,10 +261,11 @@ trait Rules extends DimRules {
   val ruleNCycleNextLast1 = Rule(
     name = "n <cycle> next/last 1: <duration> 之后",
     pattern = List(isNotLatentDuration.predicate, "((之|以)?(后|前))|过后".regex),
-    prod = tokens {
-      case Token(Duration, DurationData(v, grain, false, _, _)) :: Token(_, GroupMatch(s :: _)) :: _ =>
+    prod = optTokens {
+      case (options: Options, Token(Duration, DurationData(v, grain, false, _, _)) :: Token(_, GroupMatch(s :: _)) :: _) =>
         val offset = if (s.endsWith("后")) v else -v
-        tt(cycleNth(grain, offset, NoGrain))
+        val roundGrain = if (options.timeOptions.inheritGrainOfDuration) grain else NoGrain
+        tt(cycleNth(grain, offset, roundGrain))
     }
   )
 
@@ -274,9 +275,10 @@ trait Rules extends DimRules {
   val ruleNCycleNext2 = Rule(
     name = "n <cycle> next/last: 过 <duration>",
     pattern = List("过".regex, isNotLatentDuration.predicate),
-    prod = tokens {
-      case _ :: Token(Duration, DurationData(v, grain, _, _, _)) :: _ =>
-        tt(cycleNth(grain, v, NoGrain))
+    prod = optTokens {
+      case (options: Options, _ :: Token(Duration, DurationData(v, grain, _, _, _)) :: _ ) =>
+        val roundGrain = if (options.timeOptions.inheritGrainOfDuration) grain else NoGrain
+        tt(cycleNth(grain, v, roundGrain))
     }
   )
 
@@ -286,9 +288,10 @@ trait Rules extends DimRules {
   val ruleNCycleNext3 = Rule(
     name = "n <cycle> next/last 3:过 <duration> 之后",
     pattern = List("过".regex, isNotLatentDuration.predicate, "之?(后|前)".regex),
-    prod = tokens {
-      case _ :: Token(Duration, DurationData(v, grain, _, _, _)) :: _ =>
-        tt(cycleNth(grain, v, NoGrain))
+    prod = optTokens {
+      case (options: Options, _ :: Token(Duration, DurationData(v, grain, _, _, _)) :: _ ) =>
+        val roundGrain = if (options.timeOptions.inheritGrainOfDuration) grain else NoGrain
+        tt(cycleNth(grain, v, roundGrain))
     }
   )
 
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 6b489c96..f748cecf 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
@@ -67,9 +67,13 @@ case object Time extends Dimension with Rules with Examples {
   *
   * @param resetTimeOfDay 上午是否总是需要指今天的上午，默认是未来一个上午
   * @param recentInFuture 最近是向前计算还是向后计算，默认是未来
-	* @param alwaysInFuture 对于过去的日期是否取未来一年的日期
+  * @param alwaysInFuture 对于过去的日期是否取未来一年的日期
+  * @param inheritGrainOfDuration 继承来自持续时间的粒度，比如"三天后"，返回天级
   */
-case class TimeOptions(resetTimeOfDay: Boolean = false, recentInFuture: Boolean = true, alwaysInFuture: Boolean = true)
+case class TimeOptions(resetTimeOfDay: Boolean = false,
+                       recentInFuture: Boolean = true,
+                       alwaysInFuture: Boolean = true,
+                       inheritGrainOfDuration: Boolean = false)
 
 case class TimeData(timePred: TimePredicate,
                     latent: Boolean = false,
diff --git a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/task/NaiveBayesConsole.scala b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/task/NaiveBayesConsole.scala
index e14645e5..7ceb053c 100644
--- a/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/task/NaiveBayesConsole.scala
+++ b/duckling-fork-chinese/core/src/main/scala/com/xiaomi/duckling/task/NaiveBayesConsole.scala
@@ -41,14 +41,14 @@ import com.xiaomi.duckling.ranking.{Ranker, Testing}
 import com.xiaomi.duckling.types.Node
 
 /**
- * sbt duckConsole的jline启动有点问题，可以使用sbt console
- * @example <p>sbt core/console
- *          <p>> com.xiaomi.duckling.task.NaiveBayesConsole.run()
- *          <p>> dimension time duration
- *          <p>> 今天的天气
- *          <p>> option with-latent false
- *          <p>...
- */
+  * sbt duckConsole的jline启动有点问题，可以使用sbt console
+  * @example <p>sbt core/console
+  *          <p>> com.xiaomi.duckling.task.NaiveBayesConsole.run()
+  *          <p>> dimension time duration
+  *          <p>> 今天的天气
+  *          <p>> option with-latent false
+  *          <p>...
+  */
 object NaiveBayesConsole extends LazyLogging {
   private val context =
     Testing.testContext.copy(referenceTime = ZonedDateTime.now())
@@ -70,7 +70,12 @@ object NaiveBayesConsole extends LazyLogging {
 
     val options = new ArgumentCompleter(
       new StringsCompleter("option"),
-      new StringsCompleter("winner-only", "with-latent", "full"),
+      new StringsCompleter(
+        "winner-only",
+        "with-latent",
+        "full",
+        "inherit-duration-grain"
+      ),
       NullCompleter.INSTANCE
     )
 
@@ -117,7 +122,12 @@ object NaiveBayesConsole extends LazyLogging {
                 )
               case "with-latent" => options.copy(withLatent = value)
               case "full"        => options.copy(full = value)
-              case _             => options
+              case "inherit-duration-grain" =>
+                options.withTimeOptions(
+                  timeOptions =
+                    options.timeOptions.copy(inheritGrainOfDuration = value)
+                )
+              case _ => options
             }
             opt
           } else {


Test patch:
diff --git a/duckling-fork-chinese/core/src/test/scala/com/xiaomi/duckling/dimension/time/TimeDataTest.scala b/duckling-fork-chinese/core/src/test/scala/com/xiaomi/duckling/dimension/time/TimeDataTest.scala
index 753133e1..d39d03f3 100644
--- a/duckling-fork-chinese/core/src/test/scala/com/xiaomi/duckling/dimension/time/TimeDataTest.scala
+++ b/duckling-fork-chinese/core/src/test/scala/com/xiaomi/duckling/dimension/time/TimeDataTest.scala
@@ -24,6 +24,7 @@ import com.xiaomi.duckling.dimension.time.Types._
 import com.xiaomi.duckling.dimension.time.enums.Grain
 import com.xiaomi.duckling.dimension.time.predicates.TimeDatePredicate
 import com.xiaomi.duckling.ranking.Testing.{testContext, testOptions}
+import com.xiaomi.duckling.Types.Options
 import com.xiaomi.duckling.UnitSpec
 
 class TimeDataTest extends UnitSpec with LazyLogging {
@@ -74,5 +75,21 @@ class TimeDataTest extends UnitSpec with LazyLogging {
           }
       }
     }
+
+    it("Grain of duration inheritance") {
+      val options = testOptions.copy(targets = Set(Time))
+
+      def parse(query: String, options: Options): InstantValue = {
+        analyze(query, testContext, options).get.head.token.value
+          .asInstanceOf[TimeValue].timeValue
+          .asInstanceOf[SimpleValue].instant
+      }
+
+      val options1 = options.withTimeOptions(new TimeOptions(inheritGrainOfDuration = false))
+      parse("三天后", options1).grain should (be (Grain.NoGrain) or be (Grain.Second))
+
+      val options2 = options.withTimeOptions(new TimeOptions(inheritGrainOfDuration = true))
+      parse("三天后", options2).grain shouldBe Grain.Day
+    }
   }
 }