Java使用Aviator表达式记录(二)

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

AviatorScript 支持常见的类型,如数字、布尔值、字符串等等,同时将大整数、BigDecimal、正则表达式也作为一种基本类型来支持。

数字

数字包括整数和浮点数,AviatorScript 对 java 的类型做了缩减和扩展,同时保持了一致的运算符规则。

整数和算术运算

整数例如 -99、0、1、2、100……等等,对应的类型是 java 中的 long 类型。AviatorScript 中并没有 byte/short/int 等类型,统一整数类型都为 long,支持的范围也跟 java 语言一样:-9223372036854774808~9223372036854774807。

整数也可以用十六进制表示,以 0x 或者 0X 开头的数字,比如 0xFF(255)、0xAB(171) 等等。

整数可以参与所有的算术运算,比如加减乘除和取模等等。

let a = 99;
let b = 0xFF;
let c = -99;

println(a + b);
println(a / b);
println(a- b + c);
println(a + b * c);
println(a- (b - c));
println(a/b * b + a % b);
复制代码

加减乘除对应的运算符就是 +,-,*,/ 这都比较好理解,取模运算符就是 % ,规则和语法和 java 是一样的。

需要注意,整数相除的结果仍然是整数,比如例子中的 a/b  结果就是 0,遵循 java 的整数运算规则。

运算符之间的优先级如下:

  • 单目运算符 - 取负数
  • *, / 
  • +,- 

整个规则也跟 java 的运算符优先级保持一致。你可以通过括号来强制指定优先级,比如例子中的 a-(b-c) 就是通过括号,强制先执行 b-c ,再后再被 a 减。

通常来说,复杂的算术表达式,从代码可读性和稳健角度,都推荐使用括号来强制指定优先级。

大整数(BigInt)

对于超过 long 的整数, AviatorScript 还特别提供了大整数类型的支持,对应 java.math.BigInteger  类。任何超过 long 范围的整数字面量,会自动提升为 BigInteger 对象(以下简称 BigInt),任何数字以 N 字母结尾就自动变 BigInt:

## examples/bigint.av

let a = 10223372036854774807;  ## Literal BigInteger
let b = 1000N;  ## BigInteger
let c = 1000; ## long type

println(a);

println(a + a);

println(a * a);

println(a + b + c);
复制代码

10223372036854774807 是一个远远超过 long 返回的数字,b 也是 BigInt 类型,因为它以 N 结尾,BigInt 的算术运算和一般整数没有什么两样,采用同样的算术运算符和规则,执行这段脚本将打印

10223372036854774807
20446744073709549614
104517335803944147014652834074681887249
10223372036854776807
复制代码

请注意,默认的 long 类型在计算后如果超过范围溢出后,不会自动提升为 BigInt,但是 BigInt 和 long 一起参与算术运算的时候,结果为 BigInt 类型。关于类型转换的规则,我们后面再详细介绍。

浮点数

数字除了整数之外,AviatorScript 同样支持浮点数,但是仅支持 double 类型,也就是双精度 64 位,符合 IEEE754 规范的浮点数。传入的 java float 也将转换为 double 类型。所有的浮点数都被认为是 double 类型。浮点数的表示形式有两种:

  1. 十进制的带小数点的数字,比如 1.34159265 , 0.33333 等等。
  2. 科学计数法表示,如 1e-2 , 2E3 等等,大小写字母 e 皆可。

看一个简单例子,牛顿法求平方根

## examples/square_root.av

let a = 2;
let err = 1e-15;
let root = a;

while math.abs(a - root * root) > err {
  root = (a/root + root) / 2.0;
}

println("square root of 2 is: " + root);
复制代码

这个例子稍微复杂了一点,因为我们用了后面才会讲到的 while 循环语句(参见条件语句),不过整体逻辑还是比较简单的,求 2 的平方根,我们通过不停计算 (a/root + root)/2.0 的值,看看是否在误差范围( err 指定)内,不在就继续迭代计算,否则就跳出循环打印结果:

square root of 2 is: 1.414213562373095
复制代码

浮点数的运算符跟整数一样,同样支持加减乘除,优先级也是一样。浮点数和浮点数的算术运算结果为浮点数,浮点数和整数的运算结果仍然为浮点数。

高精度计算(Decimal)

浮点数是无法用于需要精确运算的场景,比如货币运算或者物理公式运算等,这种情况下如果在 Java 里一般推荐使用 BigDecimal 类型,调用它的 add/sub 等方法来做算术运算。

AviatorScript 将 BigDecimal 作为基本类型来支持(下文简称 decimal 类型),只要浮点数以 M 结尾就会识别类型为 deicmal,例如 1.34M  、 0.333M  或者科学计数法 2e-3M 等等。

decimal 同样使用 +,-,*,/ 来做算术运算, AviatorScript 重载了这些运算符的方法,自动转成 BigDecimal 类的各种运算方法。我们把求平方根的例子改成 decimal 运算

## examples/bigdecimal.av

let a = 2M;
let err = 1e-15M;
let root = a;

while math.abs(a - root * root) > err {
  root = (a/root + root) / 2.0M;
}

println("square root of 2M is: " + root);
复制代码

运算结果 root 的类型也是 decimal 。除了 double 以外的数字类型和 decimal 一起运算,结果为 decimal。任何有 double 参与的运算,结果都为 double。 

默认运算精度是 MathContext.DECIMAL128 ,你可以通过修改引擎配置项 Options.MATH_CONTEXT 来改变。

如果你觉的为浮点数添加 M 后缀比较麻烦,希望所有浮点数都解析为 decimal ,可以开启 Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL 选项。

数字类型转换

数字类型在运算的时候,会遵循一定的类型转换规则:

  • 单一类型参与的运算,结果仍然为该类型,比如整数和整数相除仍然是整数,double 和 double 运算结果还是 double。
  • 多种类型参与的运算,按照下列顺序: long -> bigint -> decimal -> double  自动提升,比如 long 和 bigint 运算结果为 bigint, long 和 decimal 运算结果为 decimal,任何类型和 double 一起运算结果为 double

你可以通过 long(x) 将一个数字强制转化为 long,这个过程中可能丢失精度,也可以用 double(x) 将一个数字强转为 double 类型。

## examples/double.av

let a = 1;
let b = 2;

println("a/b is " + a/b);
println("a/double(b) is " + a/double(b));
复制代码

a 和 b 都是 long 类型,他们相除的结果仍然是整数, 1/2 结果为 0,但是当使用 double 函数将 b 强制转为 double 类型,两者的结果就是浮点数了:

a/b is 0
a/double(b) is 0.5
复制代码

字符串

在任何语言中,字符串都是最基本的类型,比如 java 里就是 String 类型。AviatorScript 中同样支持字符串,只要以单引号或者双引号括起来的连续字符就是一个完整的字符串对象,例如:

  • "hello world" 
  • 'hello world' 
  • "a"  或者 'a' 

字符串可以直接通过 println 函数打印。

字符串的长度可以通过 string.length 函数获取:

## examples/string.av

let a = "hello world";

println(a);
println(string.length(a));
``

打印:
复制代码

hello world
11



字符串拼接可以用 `+` 号(这又是一个运算符重载):


复制代码

examples/string.av

let a = "hello world";
let b = 'AviatorScript';

println(a);
println(string.length(a));
println(a + ',' + b + 5);

字符串拼接 `a + ',' + b + 5`  包括了数字 5 和字符串 `','` , 任何类型和字符串相加,都将拼接为字符串,这跟 java 的规则一致。因此上面最后一行代码将打印 `hello world,AviatorScript5` 。


字符串还包括其他函数,如截取字符串 `substring`,都在 `string` 这个 namespace 下,具体见[函数库列表](https://www.yuque.com/boyan-avfmj/aviatorscript/ashevw)。


### 转义



同样,和其他语言类似,遇到特殊字符,AviatorScript 中的字符串也支持转义字符,和 java 语言一样,通过 `` 来转义一个字符,比如我们想表示的字符串中有单引号,如果我们继续使用单引号来表示字符串,这时候就需要用到转义符:


复制代码

examples/escape_string.av

println('Dennis's car');
println('AviatorScript is great.\r\nLet's try it!');



特殊字符,比如 `\r` 、 `\n` 、 `\t` 等也是同样支持。上述例子我们使用了换行 `\r\n` ,将打印:


复制代码

Dennis's car
AviatorScript is great.
Let's try it!



当然,针对引号这个情况,这里你可以简单用双引号来表示字符串,就可以避免转义:

复制代码

println("Dennis 's car");



### 字符串插值(String Interpolation)

字符串拼接可以用加法,比如

复制代码

let name = "aviator";
let s = "hello," + name;


拼接后的字符串 s 就是 `hello,aviator` 。 `+` 加法对字符串拼接做了特别优化,内部会自动转化成 `StringBuilder` 来做拼接。但是对于更复杂的场景,字符串拼接的语法仍然显得过于丑陋和繁琐,因此 5.1.0 开始, AviatorScript 支持了字符串插值,一个例子:


复制代码

examples/string_interpolation.av

let name = "aviator";
let a = 1;
let b = 2;
let s = "hello, #{name}, #{a} + #{b} = #{a + b}";
p(s);


字符串中 `#{}` 括起来的表达式都将在当前上下文里自动执行求值,然后插入到最终的结果字符串,上面的例子将输出:

复制代码

hello, aviator, 1 + 2 = 3


AviatorScript 内部做了大量优化,在编译模式复用 Expression 的情况下性能比使用加法拼接字符串更快。

## 布尔类型和逻辑运算

布尔类型用于表示真和假,它只有两个值 `true` 和 `false`  分别表示真值和假值。


比较运算如大于、小于可以产生布尔值:

复制代码

examples/boolean.av

println("3 > 1 is " + (3 > 1));
println("3 >= 1 is " + (3 >= 1));
println("3 >= 3 is " + (3 >= 3));
println("3 < 1 is " + (3 < 1));
println("3 <= 1 is " + (3 <= 1));
println("3 <= 3 is " + (3 <= 3));
println("3 == 1 is " + (3 == 1));
println("3 != 1 is " + (3 != 1));


输出:

复制代码

3 > 1 is true
3 >= 1 is true
3 >= 3 is true
3 < 1 is false
3 <= 1 is false
3 <= 3 is true
3 == 1 is false
3 != 1 is true



上面演示了所有的逻辑运算符:

-   `>`  大于
-   `>=` 大于等于

<!---->

-   `<` 小于
-   `<=` 小于等于

<!---->

-   `==` 等于
-   `!=`  不等于
复制代码