Repository: tableauio/tableau (go)

Issue:
WellKnownTypes: add new tableau well-known types
## Design Goal
| Fraction | Comparator |
|------------   |--------------|
| fraction |  comparator |
| probability | ability    |
| 5  | <30  |
| 5%  | <=30%  |
| 5‰ |  >30‰|
| 5‱ | >=30‱ |


## Well-known types
- [ ] `tableau.Fraction`
- [ ] `tableau.Comparator`
- [ ] `tableau.Vector2`
- [ ] `tableau.Vector3`
- [ ] `tableau.Vector4`
- [ ] `tableau.Vector2i`
- [ ] `tableau.Vector3i`
- [ ] `tableau.Vector4i`

```protobuf
syntax = "proto3";

package tableau;

option cc_enable_arenas = true;
option go_package = "github.com/tableauio/tableau/proto/tableaupb";
option java_package = "com.tableau.protobuf";
option java_outer_classname = "TableauProto";
option java_multiple_files = true;
option objc_class_prefix = "TPB";
option csharp_namespace = "Tableau.Protobuf.Tableau";

// Supported forms:
//   - N%: percentage, e.g.: 10%
//   - N‰: per thounsand, e.g.: 10‰
//   - N‱: per ten thounsand, e.g.: 10‱
//   - N/D: 3/4
//   - N: 3 is same to 3/1
message Fraction {
  int32 num = 1; // numerator 
  int32 den = 2; // denominator
}

message Comparator {
  Sign sign = 1;
  Fraction value = 2;

  enum Sign {
    SIGN_EQUAL = 0; // ==
    SIGN_NOT_EQUAL = 1; // !=
    SIGN_LESS = 2; // <
    SIGN_LESS_OR_EQUAL = 3; // <=
    SIGN_GREATER = 4; // >
    SIGN_GREATER_OR_EQUAL = 5; // >=
  }
}

// A 2D vector using float coordinates.
// See https://docs.godotengine.org/en/stable/classes/class_vector2.html
message Vector2 {
  float x = 1;
  float y = 2;
}

// A 3D vector using float coordinates.
// See https://docs.godotengine.org/en/stable/classes/class_vector3.html
message Vector3 {
  float x = 1;
  float y = 2;
  float z = 3;
}

// A 4D vector using float coordinates.
// See https://docs.godotengine.org/en/stable/classes/class_vector4.html
message Vector4 {
  float x = 1;
  float y = 2;
  float z = 3;
  float w = 4;
}

// A 2D vector using int32 coordinates.
// See https://docs.godotengine.org/en/stable/classes/class_vector2i.html
message Vector2i {
  int32 x = 1;
  int32 y = 2;
}

// A 3D vector using int32 coordinates.
// See https://docs.godotengine.org/en/stable/classes/class_vector3i.html
message Vector3i {
  int32 x = 1;
  int32 y = 2;
  int32 z = 3;
}

// A 4D vector using int32 coordinates.
// See https://docs.godotengine.org/en/stable/classes/class_vector4i.html
message Vector4i {
  int32 x = 1;
  int32 y = 2;
  int32 z = 3;
  int32 w = 4;
}
```
## References
- https://en.wikipedia.org/wiki/Percent_sign
- https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/timestamp.proto
- https://protobuf.dev/reference/protobuf/google.protobuf/
- https://go.dev/ref/spec#Comparison_operators

Pull request description:
WellKnownTypes: add new tableau well-known types
- close #134

Patch:
diff --git a/internal/localizer/i18n/config/ecode/en.yaml b/internal/localizer/i18n/config/ecode/en.yaml
index 9afb145..4934aad 100644
--- a/internal/localizer/i18n/config/ecode/en.yaml
+++ b/internal/localizer/i18n/config/ecode/en.yaml
@@ -88,6 +88,14 @@ E2018:
   desc: map key not exist
   text: 'map key field "{{.KeyName}}" not exist'
   help: ensure the key field is set in elements of map
+E2019:
+  desc: invalid fraction pattern
+  text: '"{{.Value}}" cannot be parsed by pattern: "N/D", {{.Error}}'
+  help: "available patterns: 5, 5/6, 10%, 10‰, 10‱"
+E2020:
+  desc: invalid comparator pattern
+  text: '"{{.Value}}" cannot be parsed by pattern: "<CompareOperator><Fraction>", {{.Error}}'
+  help: "available patterns: ==5, !=5, <3/5, <=10%, >10‰, >=10‱"
 # [3000, 3999]: importer error
 E3000:
   desc: no workbook file found about sheet specifier
diff --git a/internal/localizer/i18n/config/ecode/zh.yaml b/internal/localizer/i18n/config/ecode/zh.yaml
index abe8765..76de0be 100644
--- a/internal/localizer/i18n/config/ecode/zh.yaml
+++ b/internal/localizer/i18n/config/ecode/zh.yaml
@@ -88,6 +88,14 @@ E2018:
   desc: map key not exist
   text: 'map的key字段 "{{.KeyName}}" 不存在'
   help: 请确保map的每个元素中都填写了key字段
+E2019:
+  desc: invalid fraction pattern
+  text: '"{{.Value}}" 无法以分数形式 "分子/分母" 解析, {{.Error}}'
+  help: "支持的分数形式: 5, 5/6, 10%, 10‰, 10‱"
+E2020:
+  desc: invalid comparator pattern
+  text: '"{{.Value}}" 无法以比较式 "<比较运算符><分数>" 解析, {{.Error}}'
+  help: "支持的比较形式: ==5, !=5, <3/5, <=10%, >10‰, >=10‱"
 # [3000, 3999]: importer error
 E3000:
   desc: no workbook file found about sheet specifier
diff --git a/internal/protogen/exporter.go b/internal/protogen/exporter.go
index 518d72e..8d76035 100644
--- a/internal/protogen/exporter.go
+++ b/internal/protogen/exporter.go
@@ -283,10 +283,9 @@ func (x *sheetExporter) exportField(depth int, tagid int, field *tableaupb.Field
 		fullTypeName = field.MapEntry.ValueFullType
 	}
 
-	if fullTypeName == types.WellKnownMessageTimestamp {
-		x.Imports[timestampProtoPath] = true
-	} else if fullTypeName == types.WellKnownMessageDuration {
-		x.Imports[durationProtoPath] = true
+	if types.IsWellKnownMessage(fullTypeName) {
+		importPath := types.GetWellKnownMessageImport(fullTypeName)
+		x.Imports[importPath] = true
 	}
 
 	if field.Predefined {
diff --git a/internal/protogen/parser.go b/internal/protogen/parser.go
index 50d4259..d1af3a4 100644
--- a/internal/protogen/parser.go
+++ b/internal/protogen/parser.go
@@ -19,9 +19,7 @@ import (
 )
 
 const (
-	tableauProtoPath   = "tableau/protobuf/tableau.proto"
-	timestampProtoPath = "google/protobuf/timestamp.proto"
-	durationProtoPath  = "google/protobuf/duration.proto"
+	tableauProtoPath = "tableau/protobuf/tableau.proto"
 )
 
 const (
diff --git a/internal/types/types.go b/internal/types/types.go
index 243fc82..bd101d5 100644
--- a/internal/types/types.go
+++ b/internal/types/types.go
@@ -357,8 +357,10 @@ func init() {
 		"bytes":    ScalarKind,
 
 		// well-known message type
-		WellKnownMessageTimestamp: ScalarKind,
-		WellKnownMessageDuration:  ScalarKind,
+		WellKnownMessageTimestamp:  ScalarKind,
+		WellKnownMessageDuration:   ScalarKind,
+		WellKnownMessageFraction:   ScalarKind,
+		WellKnownMessageComparator: ScalarKind,
 
 		// "enum":     EnumKind,
 		// "repeated": ListKind,
@@ -397,6 +399,20 @@ func ParseTypeDescriptor(rawType string) *Descriptor {
 			Predefined: true,
 			Kind:       ScalarKind,
 		}
+	case "fraction":
+		return &Descriptor{
+			Name:       WellKnownMessageFraction,
+			FullName:   WellKnownMessageFraction,
+			Predefined: true,
+			Kind:       ScalarKind,
+		}
+	case "comparator":
+		return &Descriptor{
+			Name:       WellKnownMessageComparator,
+			FullName:   WellKnownMessageComparator,
+			Predefined: true,
+			Kind:       ScalarKind,
+		}
 	default:
 		desc := &Descriptor{
 			Name:       rawType,
diff --git a/internal/types/wellknown.go b/internal/types/wellknown.go
index 00db5f7..b1ad4fb 100644
--- a/internal/types/wellknown.go
+++ b/internal/types/wellknown.go
@@ -1,19 +1,27 @@
 package types
 
 const (
-	WellKnownMessageTimestamp = "google.protobuf.Timestamp"
-	WellKnownMessageDuration  = "google.protobuf.Duration"
+	WellKnownMessageTimestamp  = "google.protobuf.Timestamp"
+	WellKnownMessageDuration   = "google.protobuf.Duration"
+	WellKnownMessageFraction   = "tableau.Fraction"
+	WellKnownMessageComparator = "tableau.Comparator"
 )
 
-var wellKnownMessages map[string]bool
+var wellKnownMessages map[string]string
 
 func init() {
-	wellKnownMessages = map[string]bool{
-		WellKnownMessageTimestamp: true,
-		WellKnownMessageDuration:  true,
+	wellKnownMessages = map[string]string{
+		WellKnownMessageTimestamp:  "google/protobuf/timestamp.proto",
+		WellKnownMessageDuration:   "google/protobuf/duration.proto",
+		WellKnownMessageFraction:   "tableau/protobuf/wellknown.proto",
+		WellKnownMessageComparator: "tableau/protobuf/wellknown.proto",
 	}
 }
 
 func IsWellKnownMessage(fullTypeName string) bool {
+	return wellKnownMessages[fullTypeName] != ""
+}
+
+func GetWellKnownMessageImport(fullTypeName string) string {
 	return wellKnownMessages[fullTypeName]
 }
diff --git a/internal/xproto/value.go b/internal/xproto/value.go
index 02ab165..1663f4d 100644
--- a/internal/xproto/value.go
+++ b/internal/xproto/value.go
@@ -1,10 +1,12 @@
 package xproto
 
 import (
+	"fmt"
 	"math"
 	"strconv"
 	"strings"
 	"time"
+	"unicode"
 
 	"github.com/tableauio/tableau/internal/types"
 	"github.com/tableauio/tableau/proto/tableaupb"
@@ -30,6 +32,8 @@ var DefaultEnumValue pref.Value
 
 var DefaultTimestampValue pref.Value
 var DefaultDurationValue pref.Value
+var DefaultFractionValue pref.Value
+var DefaultComparatorValue pref.Value
 
 func init() {
 	DefaultBoolValue = pref.ValueOfBool(false)
@@ -245,6 +249,16 @@ func ParseFieldValue(fd pref.FieldDescriptor, rawValue string, locationName stri
 			// NOTE(wenchy): should not use du.ProtoReflect(), as descriptor not same.
 			// See more details at internal/xproto/build_test.go#TestCloneWellknownTypes
 			// return pref.ValueOf(du.ProtoReflect()), true, nil
+		case types.WellKnownMessageFraction:
+			if value == "" {
+				return DefaultFractionValue, false, nil
+			}
+			return parseFraction(fd.Message(), value)
+		case types.WellKnownMessageComparator:
+			if value == "" {
+				return DefaultComparatorValue, false, nil
+			}
+			return parseComparator(fd.Message(), value)
 		default:
 			return pref.Value{}, false, xerrors.Errorf("not supported message type: %s", msgName)
 		}
@@ -328,34 +342,140 @@ func parseTimeWithLocation(locationName string, timeStr string) (time.Time, erro
 	}
 }
 
-func parseDuration(duration string) (time.Duration, error) {
-	duration = strings.TrimSpace(duration)
-	if !strings.ContainsAny(duration, ":hmsµu") {
-		switch len(duration) {
+func parseDuration(val string) (time.Duration, error) {
+	val = strings.TrimSpace(val)
+	if !strings.ContainsAny(val, ":hmsµu") {
+		switch len(val) {
 		case 4:
 			// "HHmm" -> "<HH>h<mm>m", e.g.:  "1010" -> "10h10m"
-			duration = duration[0:2] + "h" + duration[2:4] + "m"
+			val = val[0:2] + "h" + val[2:4] + "m"
 		case 6:
 			// "HHmmss" -> "<HH>h<mm>m<ss>s", e.g.: "101010" -> "10h10m10s"
-			duration = duration[0:2] + "h" + duration[2:4] + "m" + duration[4:] + "s"
+			val = val[0:2] + "h" + val[2:4] + "m" + val[4:] + "s"
 		default:
 			return time.Duration(0), xerrors.Errorf(`invalid time format, please follow format like: "HHmmss" or "HHmm"`)
 		}
-	} else if strings.Contains(duration, ":") {
+	} else if strings.Contains(val, ":") {
 		// TODO: check hour < 24, minute < 60, second < 60
-		splits := strings.SplitN(duration, ":", 3)
+		splits := strings.SplitN(val, ":", 3)
 		switch len(splits) {
 		case 2:
 			// "HH:mm" -> "<HH>h<mm>m", e.g.: "10:10" -> "10h10m"
-			duration = splits[0] + "h" + splits[1] + "m"
+			val = splits[0] + "h" + splits[1] + "m"
 		case 3:
 			// "HH:mm:ss" -> "<HH>h<mm>m<ss>s", e.g.: "10:10:10" -> "10h10m10s"
-			duration = splits[0] + "h" + splits[1] + "m" + splits[2] + "s"
+			val = splits[0] + "h" + splits[1] + "m" + splits[2] + "s"
 		default:
 			return time.Duration(0), xerrors.Errorf(`invalid time format, please follow format like: "HH:mm:ss" or "HH:mm"`)
 		}
 
 	}
 
-	return time.ParseDuration(duration)
+	return time.ParseDuration(val)
+}
+
+// parseFraction parses a fraction from following forms:
+//   - N%: percentage, e.g.: 10%
+//   - N‰: per thounsand, e.g.: 10‰
+//   - N‱: per ten thounsand, e.g.: 10‱
+//   - N/D: 3/4
+//   - N: 3 is same to 3/1
+func parseFraction(md pref.MessageDescriptor, value string) (v pref.Value, present bool, err error) {
+	var numStr string
+	var den int32
+	if strings.HasSuffix(value, "%") {
+		numStr = strings.TrimSuffix(value, "%")
+		den = 100
+	} else if strings.HasSuffix(value, "‰") {
+		numStr = strings.TrimSuffix(value, "‰")
+		den = 1000
+	} else if strings.HasSuffix(value, "‱") {
+		numStr = strings.TrimSuffix(value, "‱")
+		den = 10000
+	} else if strings.Contains(value, "/") {
+		splits := strings.SplitN(value, "/", 2)
+		numStr = splits[0]
+		denStr := splits[1]
+		den, err = parseInt32(denStr)
+		if err != nil {
+			return DefaultFractionValue, false, xerrors.E2019(value, err)
+		}
+	} else {
+		numStr = value
+		den = 1
+	}
+	num, err := parseInt32(numStr)
+	if err != nil {
+		return DefaultFractionValue, false, xerrors.E2019(value, err)
+	}
+	msg := dynamicpb.NewMessage(md)
+	msg.Set(md.Fields().ByName("num"), pref.ValueOfInt32(num))
+	msg.Set(md.Fields().ByName("den"), pref.ValueOfInt32(den))
+	return pref.ValueOfMessage(msg.ProtoReflect()), true, nil
+}
+
+func parseComparator(md pref.MessageDescriptor, value string) (v pref.Value, present bool, err error) {
+	index, err := findFirstDigitOrSignIndex(value)
+	if err != nil {
+		return DefaultComparatorValue, false, xerrors.E2020(value, err)
+	}
+	// split the string into two parts
+	signStr := strings.TrimSpace(value[:index])
+	fractionStr := strings.TrimSpace(value[index:])
+	var sign tableaupb.Comparator_Sign
+	switch signStr {
+	case "==":
+		sign = tableaupb.Comparator_SIGN_EQUAL
+	case "!=":
+		sign = tableaupb.Comparator_SIGN_NOT_EQUAL
+	case "<":
+		sign = tableaupb.Comparator_SIGN_LESS
+	case "<=":
+		sign = tableaupb.Comparator_SIGN_LESS_OR_EQUAL
+	case ">":
+		sign = tableaupb.Comparator_SIGN_GREATER
+	case ">=":
+		sign = tableaupb.Comparator_SIGN_GREATER_OR_EQUAL
+	default:
+		err := fmt.Errorf("unknown comparator sign: %s", signStr)
+		return DefaultComparatorValue, false, xerrors.E2020(value, err)
+	}
+
+	msg := dynamicpb.NewMessage(md)
+	// sign
+	signFD := md.Fields().ByName("sign")
+	signVal, _, err := parseEnumValue(signFD, sign.String())
+	if err != nil {
+		return DefaultComparatorValue, false, err
+	}
+	msg.Set(signFD, signVal)
+	// fraction
+	valueFD := md.Fields().ByName("value")
+	valueMD := valueFD.Message()
+	valueVal, _, err := parseFraction(valueMD, fractionStr)
+	if err != nil {
+		return DefaultComparatorValue, false, err
+	}
+	msg.Set(valueFD, valueVal)
+	return pref.ValueOfMessage(msg.ProtoReflect()), true, nil
+}
+
+func findFirstDigitOrSignIndex(s string) (int, error) {
+	for i, char := range s {
+		if unicode.IsDigit(char) || char == '-' || char == '+' {
+			return i, nil
+		}
+	}
+	return 0, fmt.Errorf("number part not found")
+}
+
+func parseInt32(value string) (int32, error) {
+	val, err := strconv.ParseInt(value, 10, 32)
+	if err != nil {
+		return 0, err
+	}
+	if val < math.MinInt32 || val > math.MaxInt32 {
+		return 0, xerrors.E2000("int32", value, math.MinInt32, math.MaxInt32)
+	}
+	return int32(val), nil
 }
diff --git a/proto/tableau/protobuf/wellknown.proto b/proto/tableau/protobuf/wellknown.proto
new file mode 100644
index 0000000..e3cf34a
--- /dev/null
+++ b/proto/tableau/protobuf/wellknown.proto
@@ -0,0 +1,94 @@
+// Protoconf - Tableau's data interchange format
+// https://tableauio.github.io/
+
+syntax = "proto3";
+
+package tableau;
+
+option cc_enable_arenas = true;
+option go_package = "github.com/tableauio/tableau/proto/tableaupb";
+option java_package = "com.tableau.protobuf";
+option java_outer_classname = "TableauProto";
+option java_multiple_files = true;
+option objc_class_prefix = "TPB";
+option csharp_namespace = "Tableau.Protobuf.Tableau";
+
+import "tableau/protobuf/tableau.proto";
+
+// Supported forms:
+//  - N%: percentage, e.g.: 10%
+//  - N‰: per thounsand, e.g.: 10‰
+//  - N‱: per ten thounsand, e.g.: 10‱
+//  - N/D: 3/4
+//  - N: 3 is same to 3/1
+message Fraction {
+  int32 num = 1;  // numerator
+  int32 den = 2;  // denominator
+}
+
+message Comparator {
+  Sign sign = 1;
+  Fraction value = 2;
+
+  enum Sign {
+    SIGN_EQUAL = 0;             // ==
+    SIGN_NOT_EQUAL = 1;         // !=
+    SIGN_LESS = 2;              // <
+    SIGN_LESS_OR_EQUAL = 3;     // <=
+    SIGN_GREATER = 4;           // >
+    SIGN_GREATER_OR_EQUAL = 5;  // >=
+  }
+}
+
+// TODO:
+// - Vector2d for double 2D vector
+// - Vector2l for int64 (long) 2D vector
+// ...
+
+// A 2D vector using float coordinates.
+// See https://docs.godotengine.org/en/stable/classes/class_vector2.html
+message Vector2 {
+  float x = 1 [(tableau.field) = { name: "X" }];
+  float y = 2 [(tableau.field) = { name: "Y" }];
+}
+
+// A 3D vector using float coordinates.
+// See https://docs.godotengine.org/en/stable/classes/class_vector3.html
+message Vector3 {
+  float x = 1 [(tableau.field) = { name: "X" }];
+  float y = 2 [(tableau.field) = { name: "Y" }];
+  float z = 3 [(tableau.field) = { name: "Z" }];
+}
+
+// A 4D vector using float coordinates.
+// See https://docs.godotengine.org/en/stable/classes/class_vector4.html
+message Vector4 {
+  float x = 1 [(tableau.field) = { name: "X" }];
+  float y = 2 [(tableau.field) = { name: "Y" }];
+  float z = 3 [(tableau.field) = { name: "Z" }];
+  float w = 4 [(tableau.field) = { name: "W" }];
+}
+
+// A 2D vector using int32 coordinates.
+// See https://docs.godotengine.org/en/stable/classes/class_vector2i.html
+message Vector2i {
+  int32 x = 1 [(tableau.field) = { name: "X" }];
+  int32 y = 2 [(tableau.field) = { name: "Y" }];
+}
+
+// A 3D vector using int32 coordinates.
+// See https://docs.godotengine.org/en/stable/classes/class_vector3i.html
+message Vector3i {
+  int32 x = 1 [(tableau.field) = { name: "X" }];
+  int32 y = 2 [(tableau.field) = { name: "Y" }];
+  int32 z = 3 [(tableau.field) = { name: "Z" }];
+}
+
+// A 4D vector using int32 coordinates.
+// See https://docs.godotengine.org/en/stable/classes/class_vector4i.html
+message Vector4i {
+  int32 x = 1 [(tableau.field) = { name: "X" }];
+  int32 y = 2 [(tableau.field) = { name: "Y" }];
+  int32 z = 3 [(tableau.field) = { name: "Z" }];
+  int32 w = 4 [(tableau.field) = { name: "W" }];
+}
diff --git a/xerrors/ecode.go b/xerrors/ecode.go
index a53771d..5233139 100644
--- a/xerrors/ecode.go
+++ b/xerrors/ecode.go
@@ -189,6 +189,22 @@ func E2018(keyName string) error {
 	})
 }
 
+// E2019: invalid fraction pattern.
+func E2019(value string, err error) error {
+	return renderEcode("E2019", map[string]any{
+		"Value": value,
+		"Error": err,
+	})
+}
+
+// E2020: invalid comparator pattern.
+func E2020(value string, err error) error {
+	return renderEcode("E2020", map[string]any{
+		"Value": value,
+		"Error": err,
+	})
+}
+
 // E3000: no workbook file found about sheet specifier.
 func E3000(sheetSpecifier, pattern string) error {
 	return renderEcode("E3000", map[string]any{


Test patch:
diff --git a/internal/types/types_test.go b/internal/types/types_test.go
index e5586ba..c8eb299 100644
--- a/internal/types/types_test.go
+++ b/internal/types/types_test.go
@@ -502,6 +502,16 @@ func TestIsScalarType(t *testing.T) {
 			args: args{t: WellKnownMessageDuration},
 			want: true,
 		},
+		{
+			name: WellKnownMessageFraction,
+			args: args{t: WellKnownMessageFraction},
+			want: true,
+		},
+		{
+			name: WellKnownMessageComparator,
+			args: args{t: WellKnownMessageComparator},
+			want: true,
+		},
 		{
 			name: "MessageType",
 			args: args{t: "MessageType"},
diff --git a/internal/xproto/value_test.go b/internal/xproto/value_test.go
index 342bc50..c351c0a 100644
--- a/internal/xproto/value_test.go
+++ b/internal/xproto/value_test.go
@@ -5,6 +5,7 @@ import (
 	"math"
 	"testing"
 
+	"github.com/tableauio/tableau/proto/tableaupb"
 	pref "google.golang.org/protobuf/reflect/protoreflect"
 	"google.golang.org/protobuf/types/known/wrapperspb"
 )
@@ -147,7 +148,7 @@ func TestParseFieldValue(t *testing.T) {
 				fd:       uint64ValueFd,
 				rawValue: "18446744073709551615 ",
 			},
-			wantV:       pref.ValueOfUint64(18446744073709551615 ),
+			wantV:       pref.ValueOfUint64(18446744073709551615),
 			wantPresent: true,
 			wantErr:     false,
 		},
@@ -359,3 +360,238 @@ func TestParseFieldValue(t *testing.T) {
 		})
 	}
 }
+
+func Test_parseFraction(t *testing.T) {
+	msg := &tableaupb.Fraction{}
+	md := msg.ProtoReflect().Descriptor()
+	type args struct {
+		value string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantF   *tableaupb.Fraction
+		wantErr bool
+	}{
+		{
+			name: "percentage",
+			args: args{
+				value: "10%",
+			},
+			wantF: &tableaupb.Fraction{
+				Num: 10,
+				Den: 100,
+			},
+		},
+		{
+			name: "per-thounsand",
+			args: args{
+				value: "10‰",
+			},
+			wantF: &tableaupb.Fraction{
+				Num: 10,
+				Den: 1000,
+			},
+		},
+		{
+			name: "per-ten-thounsand",
+			args: args{
+				value: "10‱",
+			},
+			wantF: &tableaupb.Fraction{
+				Num: 10,
+				Den: 10000,
+			},
+		},
+		{
+			name: "num-den",
+			args: args{
+				value: "3/4",
+			},
+			wantF: &tableaupb.Fraction{
+				Num: 3,
+				Den: 4,
+			},
+		},
+		{
+			name: "only-num",
+			args: args{
+				value: "10",
+			},
+			wantF: &tableaupb.Fraction{
+				Num: 10,
+				Den: 1,
+			},
+		},
+		{
+			name: "negative",
+			args: args{
+				value: "-6/10",
+			},
+			wantF: &tableaupb.Fraction{
+				Num: -6,
+				Den: 10,
+			},
+		},
+		{
+			name: "positive",
+			args: args{
+				value: "+6/10",
+			},
+			wantF: &tableaupb.Fraction{
+				Num: +6,
+				Den: 10,
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotF, _, err := parseFraction(md, tt.args.value)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseFraction() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if err == nil && !gotF.Equal(pref.ValueOfMessage(tt.wantF.ProtoReflect())) {
+				t.Errorf("parseFraction() = %v, want %v", gotF, tt.wantF)
+			}
+		})
+	}
+}
+
+func Test_parseComparator(t *testing.T) {
+	msg := &tableaupb.Comparator{}
+	md := msg.ProtoReflect().Descriptor()
+	type args struct {
+		value string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantC   *tableaupb.Comparator
+		wantErr bool
+	}{
+		{
+			name: "equal",
+			args: args{
+				value: "==10%",
+			},
+			wantC: &tableaupb.Comparator{
+				Sign: tableaupb.Comparator_SIGN_EQUAL,
+				Value: &tableaupb.Fraction{
+					Num: 10,
+					Den: 100,
+				},
+			},
+		},
+		{
+			name: "not-equal",
+			args: args{
+				value: "!=10",
+			},
+			wantC: &tableaupb.Comparator{
+				Sign: tableaupb.Comparator_SIGN_NOT_EQUAL,
+				Value: &tableaupb.Fraction{
+					Num: 10,
+					Den: 1,
+				},
+			},
+		},
+		{
+			name: "less",
+			args: args{
+				value: "<10%",
+			},
+			wantC: &tableaupb.Comparator{
+				Sign: tableaupb.Comparator_SIGN_LESS,
+				Value: &tableaupb.Fraction{
+					Num: 10,
+					Den: 100,
+				},
+			},
+		},
+		{
+			name: "less-or-equal",
+			args: args{
+				value: "<=10%",
+			},
+			wantC: &tableaupb.Comparator{
+				Sign: tableaupb.Comparator_SIGN_LESS_OR_EQUAL,
+				Value: &tableaupb.Fraction{
+					Num: 10,
+					Den: 100,
+				},
+			},
+		},
+		{
+			name: "greater-with-space",
+			args: args{
+				value: "> 10%",
+			},
+			wantC: &tableaupb.Comparator{
+				Sign: tableaupb.Comparator_SIGN_GREATER,
+				Value: &tableaupb.Fraction{
+					Num: 10,
+					Den: 100,
+				},
+			},
+		},
+		{
+			name: "greater-or-equal-with-two-space",
+			args: args{
+				value: ">=  10%",
+			},
+			wantC: &tableaupb.Comparator{
+				Sign: tableaupb.Comparator_SIGN_GREATER_OR_EQUAL,
+				Value: &tableaupb.Fraction{
+					Num: 10,
+					Den: 100,
+				},
+			},
+		},
+		{
+			name: "negative",
+			args: args{
+				value: ">=-10%",
+			},
+			wantC: &tableaupb.Comparator{
+				Sign: tableaupb.Comparator_SIGN_GREATER_OR_EQUAL,
+				Value: &tableaupb.Fraction{
+					Num: -10,
+					Den: 100,
+				},
+			},
+		},
+		{
+			name: "positive",
+			args: args{
+				value: ">=+10%",
+			},
+			wantC: &tableaupb.Comparator{
+				Sign: tableaupb.Comparator_SIGN_GREATER_OR_EQUAL,
+				Value: &tableaupb.Fraction{
+					Num: +10,
+					Den: 100,
+				},
+			},
+		},
+		{
+			name: "invalid",
+			args: args{
+				value: ">==10%",
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotC, _, err := parseComparator(md, tt.args.value)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseComparator() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if err == nil && !gotC.Equal(pref.ValueOfMessage(tt.wantC.ProtoReflect())) {
+				t.Errorf("parseComparator() = %v, want %v", gotC, tt.wantC)
+			}
+		})
+	}
+}
diff --git a/test/functest/conf/WellKnownTypeComparator.json b/test/functest/conf/WellKnownTypeComparator.json
new file mode 100644
index 0000000..eeddf4a
--- /dev/null
+++ b/test/functest/conf/WellKnownTypeComparator.json
@@ -0,0 +1,46 @@
+{
+    "minRatio": {
+        "sign": "SIGN_NOT_EQUAL",
+        "value": {
+            "num": 1,
+            "den": 4
+        }
+    },
+    "ratioList": [
+        {
+            "sign": "SIGN_LESS",
+            "value": {
+                "num": 10,
+                "den": 100
+            }
+        },
+        {
+            "sign": "SIGN_LESS_OR_EQUAL",
+            "value": {
+                "num": 10,
+                "den": 1000
+            }
+        },
+        {
+            "sign": "SIGN_GREATER",
+            "value": {
+                "num": 10,
+                "den": 10000
+            }
+        },
+        {
+            "sign": "SIGN_GREATER_OR_EQUAL",
+            "value": {
+                "num": 10,
+                "den": 1
+            }
+        },
+        {
+            "sign": "SIGN_EQUAL",
+            "value": {
+                "num": 3,
+                "den": 5
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/test/functest/conf/WellKnownTypeFraction.json b/test/functest/conf/WellKnownTypeFraction.json
new file mode 100644
index 0000000..8f1774c
--- /dev/null
+++ b/test/functest/conf/WellKnownTypeFraction.json
@@ -0,0 +1,24 @@
+{
+    "minRatio": {
+        "num": 1,
+        "den": 4
+    },
+    "ratioList": [
+        {
+            "num": 10,
+            "den": 100
+        },
+        {
+            "num": 10,
+            "den": 1000
+        },
+        {
+            "num": 10,
+            "den": 10000
+        },
+        {
+            "num": 10,
+            "den": 1
+        }
+    ]
+}
\ No newline at end of file
diff --git a/test/functest/proto/excel__wellknown__well_known.proto b/test/functest/proto/excel__wellknown__well_known.proto
index 2e63e20..7aa3a7d 100644
--- a/test/functest/proto/excel__wellknown__well_known.proto
+++ b/test/functest/proto/excel__wellknown__well_known.proto
@@ -1,4 +1,4 @@
-// Code generated by tableau (protogen v0.5.0). DO NOT EDIT.
+// Code generated by tableau (protogen v0.6.1). DO NOT EDIT.
 // clang-format off
 
 syntax = "proto3";
@@ -8,10 +8,18 @@ package protoconf;
 import "google/protobuf/duration.proto";
 import "google/protobuf/timestamp.proto";
 import "tableau/protobuf/tableau.proto";
+import "tableau/protobuf/wellknown.proto";
 
 option go_package = "github.com/tableauio/tableau/test/functest/protoconf";
 option (tableau.workbook) = {name:"excel/wellknown/WellKnown#*.csv"};
 
+message WellKnownTypeComparator {
+  option (tableau.worksheet) = {name:"WellKnownTypeComparator" namerow:1 typerow:2 noterow:3 datarow:4};
+
+  tableau.Comparator min_ratio = 1 [(tableau.field) = {name:"MinRatio"}];
+  repeated tableau.Comparator ratio_list = 2 [(tableau.field) = {name:"Ratio" layout:LAYOUT_HORIZONTAL}];
+}
+
 message WellKnownTypeDate {
   option (tableau.worksheet) = {name:"WellKnownTypeDate" namerow:1 typerow:2 noterow:3 datarow:4};
 
@@ -20,6 +28,13 @@ message WellKnownTypeDate {
   repeated google.protobuf.Timestamp trigger_list = 3 [(tableau.field) = {name:"Trigger" layout:LAYOUT_INCELL}];
 }
 
+message WellKnownTypeFraction {
+  option (tableau.worksheet) = {name:"WellKnownTypeFraction" namerow:1 typerow:2 noterow:3 datarow:4};
+
+  tableau.Fraction min_ratio = 1 [(tableau.field) = {name:"MinRatio"}];
+  repeated tableau.Fraction ratio_list = 2 [(tableau.field) = {name:"Ratio" layout:LAYOUT_HORIZONTAL}];
+}
+
 message WellKnownTypeTime {
   option (tableau.worksheet) = {name:"WellKnownTypeTime" namerow:1 typerow:2 noterow:3 datarow:4};
 
diff --git a/test/functest/testdata/excel/wellknown/WellKnown#WellKnownTypeComparator.csv b/test/functest/testdata/excel/wellknown/WellKnown#WellKnownTypeComparator.csv
new file mode 100644
index 0000000..3c8a0e6
--- /dev/null
+++ b/test/functest/testdata/excel/wellknown/WellKnown#WellKnownTypeComparator.csv
@@ -0,0 +1,4 @@
+MinRatio,Ratio1,Ratio2,Ratio3,Ratio4,Ratio5
+comparator,[]comparator,comparator,comparator,comparator,comparator
+min ratio,ratio1,ratio 2,ratio 3,ratio 4,ratio 5
+!=1/4,<10%,<=10‰,>10‱,>=10,==3/5
diff --git a/test/functest/testdata/excel/wellknown/WellKnown#WellKnownTypeFraction.csv b/test/functest/testdata/excel/wellknown/WellKnown#WellKnownTypeFraction.csv
new file mode 100644
index 0000000..d0faf0f
--- /dev/null
+++ b/test/functest/testdata/excel/wellknown/WellKnown#WellKnownTypeFraction.csv
@@ -0,0 +1,4 @@
+MinRatio,Ratio1,Ratio2,Ratio3,Ratio4
+fraction,[]fraction,fraction,fraction,fraction
+min ratio,ratio1,ratio 2,ratio 3,ratio 4
+1/4,10%,10‰,10‱,10