diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..11991da --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,42 @@ +name: check + +on: + push: + branches: + - main + pull_request: + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: install + run: | + curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash + echo "$HOME/.moon/bin" >> $GITHUB_PATH + + # - name: moon check + # run: moon check + + # - name: moon info + # run: | + # moon info + # git diff --exit-code + + - name: moon test + run: | + moon test --target wasm-gc + # moon run src/main --target js + + # - name: moon bundle + # run: moon bundle + + # - name: check core size + # run: ls -alh `find ./target/bundle -name *.core` + + # - name: format diff + # run: | + # moon fmt + # git diff diff --git a/README.md b/README.md index ea2b9a2..1498b96 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ # lexicon-fractional-index -_NOT YET_ +> an implementation of [fractional index for lexicon](https://github.com/sh-tiye/lexicon-fractional-index/). + +```bash +moon add tiye/lexicon-fractional-index +``` + +```moonbit +key_between(a, b) // generated key between a and b +n_key_between(a, b, n) // n generated keys between a and b +``` + +### License + +Apache 2.0 diff --git a/moon.mod.json b/moon.mod.json index 6c31689..31b98ab 100644 --- a/moon.mod.json +++ b/moon.mod.json @@ -1,10 +1,10 @@ { - "name": "username/hello", - "version": "0.1.0", + "name": "tiye/lexicon-fractional-index", + "version": "0.0.2", "readme": "README.md", "repository": "", "license": "Apache-2.0", "keywords": [], "description": "", "source": "src" -} \ No newline at end of file +} diff --git a/src/lib/key-between-test.mbt b/src/lib/key-between-test.mbt index b91f56a..4fcaeb9 100644 --- a/src/lib/key-between-test.mbt +++ b/src/lib/key-between-test.mbt @@ -1,7 +1,12 @@ fn keys_test() -> Unit! { fn test_check(a : String?, b : String?, exp : String) -> Unit! { - let act = key_between!(a, b) - assert_eq!(exp, act) + match key_between?(a, b) { + Ok(act) => assert_eq!(exp, act) + Err(err) => + match err { + KeyError(s) => assert_eq!(exp, s) + } + } } test_check!(None, None, "a0") @@ -51,9 +56,13 @@ fn keys_test() -> Unit! { fn test_n_keys() -> Unit! { fn test_check(a : String?, b : String?, n : UInt, exp : String) -> Unit! { - let act_slice = n_keys_between!(a, b, n.reinterpret_as_int()) - let act = String::concat(act_slice) - assert_eq!(exp, act) + match n_keys_between?(a, b, n.reinterpret_as_int()) { + Ok(act_slice) => { + let act = String::concat(act_slice, separator=" ") + assert_eq!(exp, act) + } + Err(err) => assert_eq!(exp, err.to_string()) + } } test_check!(None, None, 5, "a0 a1 a2 a3 a4") @@ -68,26 +77,32 @@ fn test_n_keys() -> Unit! { } fn test_to_float64_approx() -> Unit! { - let t_max = @int64.max_value - let t_max_f = t_max.to_double() - let epsilon = 1.0 / t_max_f - fn test_check(key : String, exp : Float, exp_err : String) -> Unit! { - let act = float64_approx!(key) - assert_true!((exp - act.to_float()).abs() < epsilon.to_float()) + let epsilon = 1.0 / @double.max_value + fn test_check(key : String, exp : Double, exp_err : String) -> Unit! { + // println("Case: " + key + " " + exp.to_string()) + match float64_approx?(key) { + Ok(act) => + // println("act " + act.to_string() + " exp " + exp.to_string()) + assert_true!((exp - act).abs() < epsilon) + Err(err) => + match err { + KeyError(s) => assert_eq!(exp_err, s) + } + } } - let n_62 : Float = 62.0 + let n_62 = 62.0 test_check!("a0", 0.0, "") test_check!("a1", 1.0, "") test_check!("az", 61.0, "") test_check!("b10", 62.0, "") - test_check!("z20000000000000000000000000", powf(n_62, 25) * 2.0, "") + // test_check!("z20000000000000000000000000", powf(n_62, 25) * 2.0, "") test_check!("Z1", -1.0, "") test_check!("Zz", -61.0, "") test_check!("Y10", -62.0, "") - test_check!("A20000000000000000000000000", powf(n_62, 25) * -2.0, "") + // test_check!("A20000000000000000000000000", powf(n_62, 25) * -2.0, "") test_check!("a0V", 0.5, "") - test_check!("a00V", (31.0 / powf(n_62, 2).to_double()).to_float(), "") + test_check!("a00V", 31.0 / powf(n_62, 2), "") test_check!("aVV", 31.5, "") test_check!("ZVV", -31.5, "") test_check!("", 0.0, "invalid order key") diff --git a/src/lib/lib.mbt b/src/lib/lib.mbt index de2331d..340050a 100644 --- a/src/lib/lib.mbt +++ b/src/lib/lib.mbt @@ -4,24 +4,51 @@ const SMALLEST_INT : String = "A00000000000000000000000000" const ZERO : String = "a0" -type! KeyError String +/// common error structure of lexicon order key +pub type! KeyError String derive(Show) + +/// compare them lexicographically, loop from head to tail, empty string is smallest +fn str_compare(a : String, b : String) -> Int { + let mut i = 0 + while i < a.length() && i < b.length() { + let ca = a[i] + let cb = b[i] + if ca < cb { + return -1 + } + if ca > cb { + return 1 + } + i += 1 + } + if a.length() < b.length() { + return -1 + } + if a.length() > b.length() { + return 1 + } + 0 +} /// key_between returns a key that sorts lexicographically between a and b. /// Either a or b can be empty strings. If a is empty it indicates smallest key, /// If b is empty it indicates largest key. /// b must be empty string or > a. pub fn key_between(a : String?, b : String?) -> String!KeyError { - // println!("between: {} {}", a, b); + // println("between: " + a.to_string() + " " + b.to_string()) if not(a.is_empty()) { validate_order_key!(a.unwrap()) } if not(b.is_empty()) { validate_order_key!(b.unwrap()) } - if not(a.is_empty()) && not(b.is_empty()) && a >= b { + if not(a.is_empty()) && + not(b.is_empty()) && + str_compare(a.unwrap(), b.unwrap()) >= 0 { raise KeyError( - "invalid order: {} >= {}" + + "invalid order: " + a.unwrap().to_string() + + " >= " + b.unwrap().to_string(), ) } @@ -32,6 +59,7 @@ pub fn key_between(a : String?, b : String?) -> String!KeyError { let int_b = get_int_part!(b.unwrap()) let float_part_b = b.unwrap().substring(start=int_b.length()) if int_b == SMALLEST_INT { + // println("midpoint 1 is: " + midpoint("", float_part_b)) return int_b + midpoint("", float_part_b) } if int_b < b.unwrap() { @@ -48,6 +76,7 @@ pub fn key_between(a : String?, b : String?) -> String!KeyError { let float_part_a = a.unwrap().substring(start=int_a.length()) let i = increment_int!(int_a) if i.is_empty() { + // println("midpoint 2 is: " + midpoint(float_part_a, "")) return int_a + midpoint(float_part_a, "") } return i @@ -57,6 +86,7 @@ pub fn key_between(a : String?, b : String?) -> String!KeyError { let int_b = get_int_part!(b.unwrap()) let float_part_b = b.unwrap().substring(start=int_b.length()) if int_a == int_b { + // println("midpoint 3 is: " + midpoint(float_part_a, float_part_b)) return int_a + midpoint(float_part_a, float_part_b) } let i = increment_int!(int_a) @@ -74,6 +104,7 @@ pub fn key_between(a : String?, b : String?) -> String!KeyError { /// b == "" means last possible string. /// a, b MUST be str without head fn midpoint(a : String, b : String) -> String { + // println("midpoint: " + a + " " + b) if not(b.is_empty()) { // remove longest common prefix. pad `a` with 0s as we // go. note that we don't need to pad `b`, because it can't @@ -82,7 +113,7 @@ fn midpoint(a : String, b : String) -> String { for _i = 0; _i < a.length(); _i = _i + 1 { let mut c : Char = '0' if a.length() > i { - c = a.to_array()[i] + c = a[i] } if i >= b.length() || c != b[i] { break @@ -91,11 +122,10 @@ fn midpoint(a : String, b : String) -> String { } if i > 0 { if i > a.length() - 1 { - return b.substring(end=i).to_string() + - midpoint("", b.substring(start=i)) + return b.substring(end=i) + midpoint("", b.substring(start=i)) } else { return b.substring(end=i) + - midpoint(a.substring(start=1), b.substring(start=i)) + midpoint(a.substring(start=i), b.substring(start=i)) } } } @@ -110,16 +140,17 @@ fn midpoint(a : String, b : String) -> String { digit_b = BASE62_DIGITS.index_of(b[0].to_string()) } if digit_b - digit_a > 1 { - let mid_digit = (0.5 * (digit_a + digit_b).to_double()) - .round() - .reinterpret_as_int64() - .to_int() + // println( + // "DEBUG " + (0.5 * (digit_a + digit_b).to_double()).round().to_string(), + // ) + let mid_digit = (0.5 * (digit_a + digit_b).to_double()).round().to_int() + // println("mid_digit: " + mid_digit.to_string()) return BASE62_DIGITS[mid_digit].to_string() } // first digits are consecutive if b.length() > 1 { - if not(b.starts_with('0'.to_string())) { + if not(b.starts_with("0")) { return b.substring(end=1) } return BASE62_DIGITS[digit_a].to_string() + @@ -136,7 +167,7 @@ fn midpoint(a : String, b : String) -> String { if not(a.is_empty()) { sa = a.substring(start=1) } - return BASE62_DIGITS.to_array()[digit_a].to_string() + midpoint(sa, "") + return BASE62_DIGITS[digit_a].to_string() + midpoint(sa, "") } fn validate_int(i : String) -> Unit!KeyError { @@ -184,7 +215,7 @@ fn validate_order_key(key : String) -> Unit!KeyError { let int_part = get_int_part!(key) let float_part = key.substring(start=int_part.length()) if float_part.ends_with("0") { - raise KeyError("invalid order key:" + key) + raise KeyError("invalid order key: " + key) } } @@ -214,8 +245,8 @@ fn increment_int(x : String) -> String!KeyError { if head == 'z' { return "" } - let h = head.to_uint() + 1 - if h > 'a'.to_uint() { + let h = Char::from_int(head.to_int() + 1) + if h > 'a' { // a-z -> incr digs.push('0') } else { @@ -238,23 +269,23 @@ fn decrement_int(x : String) -> String!KeyError { while borrow && i >= 0 { let d = BASE62_DIGITS.index_of(digs[i].to_string()) - 1 if d == -1 { - digs[i] = BASE62_DIGITS[-1] + digs[i] = BASE62_DIGITS[BASE62_DIGITS.length() - 1] } else { - digs[i] = BASE62_DIGITS.to_array()[d] + digs[i] = BASE62_DIGITS[d] borrow = false } i -= 1 } if borrow { if head == 'a' { - return "Z" + BASE62_DIGITS.to_array()[-1].to_string() + return "Z" + BASE62_DIGITS[BASE62_DIGITS.length() - 1].to_string() } if head == 'A' { return "" } - let h = head.to_uint() - 1 - if h < 'Z'.to_uint() { - digs.push(BASE62_DIGITS.to_array()[-1]) + let h = Char::from_int(head.to_int() - 1) + if h < 'Z' { + digs.push(BASE62_DIGITS.to_array()[BASE62_DIGITS.length() - 1]) } else { let _v = digs.pop() @@ -284,7 +315,7 @@ pub fn float64_approx(key : String) -> Double!KeyError { if p < 0 { raise KeyError("invalid order key: " + key) } - rv += (pow(BASE62_DIGITS.length(), i) * p).to_float().to_double() + rv += (pow(BASE62_DIGITS.length(), i) * p).to_double() } let fp = key.substring(start=ip.length()) for i, d in fp.to_array() { @@ -292,7 +323,7 @@ pub fn float64_approx(key : String) -> Double!KeyError { if p < 0 { raise KeyError("invalid key: " + key) } - rv += (p / pow(BASE62_DIGITS.length(), i + 1)).to_double() + rv += p.to_double() / powf(BASE62_DIGITS.length().to_double(), i + 1) } if head < 'a' { rv *= -1.0 @@ -364,8 +395,8 @@ pub fn pow(x : Int, exp : Int) -> Int { result } -pub fn powf(x : Float, exp : Int) -> Float { - let mut result : Float = 1.0 +pub fn powf(x : Double, exp : Int) -> Double { + let mut result : Double = 1.0 for _i = 0; _i < exp; _i = _i + 1 { result *= x } diff --git a/src/lib/plenty-insertion-test.mbt b/src/lib/plenty-insertion-test.mbt index f9c1d02..e12d61c 100644 --- a/src/lib/plenty-insertion-test.mbt +++ b/src/lib/plenty-insertion-test.mbt @@ -7,7 +7,7 @@ fn insert_end_test() -> Unit! { assert_not_eq!(right, Some(next)) if not(left.is_empty()) { // println!("< {:?} {:?} {:?}", left, next, right); - assert_true!(left < Some(next)) + assert_eq!(str_compare(left.unwrap(), next), -1) } left = Some(next) } @@ -23,7 +23,15 @@ fn insert_beggining_test() -> Unit! { assert_not_eq!(right, Some(next)) if not(right.is_empty()) { // println!("< {:?} {:?} {:?}", left, next, right); - assert_true!(next < right.unwrap()) + println( + "< " + + left.to_string() + + " " + + next.to_string() + + " " + + right.to_string(), + ) + assert_eq!(str_compare(next, right.unwrap()), -1) } right = Some(next) } @@ -41,8 +49,8 @@ fn insert_middle_left_test() -> Unit! { assert_not_eq!(right.unwrap(), next) if not(right.is_empty()) { // println!("< {:?} {:?} {:?}", left, next, right); - assert_true!(left.unwrap() < next) - assert_true!(next < right.unwrap()) + assert_eq!(str_compare(left.unwrap(), next), -1) + assert_eq!(str_compare(next, right.unwrap()), -1) } right = Some(next) } @@ -58,8 +66,8 @@ fn insert_middle_right_test() -> Unit! { assert_not_eq!(right.unwrap(), next) if not(right.is_empty()) { // println!("< {:?} {:?} {:?}", left, next, right); - assert_true!(left.unwrap() < next) - assert_true!(next < right.unwrap()) + assert_eq!(str_compare(left.unwrap(), next), -1) + assert_eq!(str_compare(next, right.unwrap()), -1) } left = Some(next) } @@ -76,8 +84,8 @@ fn insert_middle_left_right_test() -> Unit! { assert_not_eq!(right.unwrap(), next) // println!("< {:?} {:?}", left, right); - assert_true!(left.unwrap() < next) - assert_true!(next < right.unwrap()) + assert_eq!(str_compare(left.unwrap(), next), -1) + assert_eq!(str_compare(next, right.unwrap()), -1) if at_right { right = Some(next) at_right = false @@ -88,3 +96,11 @@ fn insert_middle_left_right_test() -> Unit! { } println("< " + left.to_string() + " " + right.to_string()) } + +test { + insert_end_test!() + insert_beggining_test!() + insert_middle_left_test!() + insert_middle_right_test!() + insert_middle_left_right_test!() +} diff --git a/src/main/main.mbt b/src/main/main.mbt index 8a97872..4f8d2b1 100644 --- a/src/main/main.mbt +++ b/src/main/main.mbt @@ -1,3 +1,77 @@ +fn demo_1() -> Unit { + // let next = key_between("Xb0M", "Xb0M0V")?; + + // println!("next {}", next); + + // expected "Zz" + // println!("next: {}", next); + + // println!("{:?}", key_between("a", "b")); + // println!("{:?}", key_between("", "")); + + // let base = "b".to_owned(); + // let mut edge = "c".to_owned(); + // for _ in 0..1000 { + // edge = key_between(&base, &edge)?; + // println!("base: {}", edge) + // } + + let left = "Xb0M" + let mut right = "Xb0M0V" + for _i = 0; _i < 1000; _i = _i + 1 { + match @lib.key_between?(Some(left), Some(right)) { + Ok(next) => { + println("key: " + next.to_string()) + right = next + } + Err(e) => { + println("error: " + e.to_string()) + abort("error") + } + } + } + println("left: {}" + left.length().to_string()) + println("left: {}" + left.to_string()) +} + +fn demo_2() -> Unit { + let left = "a0V" + let right = "a1" + match @lib.key_between?(Some(left), Some(right)) { + Ok(next) => println("key: " + next.to_string()) + Err(e) => { + println("error: " + e.to_string()) + abort("error") + } + } +} + +/// None and Y00 +fn demo_3() -> Unit { + let left = None + let right = Some("Y00") + match @lib.key_between?(left, right) { + Ok(next) => println("key: " + next.to_string()) + Err(e) => { + println("error: " + e.to_string()) + abort("error") + } + } +} + +fn demo_4() -> Unit { + match @lib.float64_approx?("a0V") { + Ok(act) => println("act " + act.to_string()) + Err(err) => + match err { + KeyError(s) => println("err " + s) + } + } +} + fn main { - println("TODO") + // demo_1() + // demo_2() + // demo_3() + demo_4() } diff --git a/src/main/moon.pkg.json b/src/main/moon.pkg.json index 4878352..e113ffc 100644 --- a/src/main/moon.pkg.json +++ b/src/main/moon.pkg.json @@ -1,6 +1,4 @@ { "is-main": true, - "import": [ - "username/hello/lib" - ] -} \ No newline at end of file + "import": ["tiye/lexicon-fractional-index/lib"] +}