许多的人使用Java来作为主要的编程语言,许多的时候感觉代码太过繁复,当然有Scala、Kotlin、Python等等语言号称可以解决此问题,但是毕竟生态圈的切换不是个小问题。同时语法结构和Java相去甚远也导致切换的成本毕竟高。
为此本人做了一下尝试,准备走一个中间路线,主题还是用Java语言,但是在需要的时候用TinyScript来解决一下问题,然后再回到Java主体执行,所以你完全可以把它当成一种EL语言来使用,当然解决复杂问题也比常规的EL语言更方便,毕竟TinyScript在集合运算能力方面有重点扩展的地方。
未来的方向,会重点放在算法方面,目前已经内嵌了动态规划的背包问题通用方法,后面会逐步扩充其他算法,让程序员们不再纠结于算法实现,而是集中注意力在问题上。
语言特性列表
- 支持有序数据结构:数组和序列
- 支持无序数据结构:set和map
- 支持专有数据结构:树和序表
- 序表支持关联、匹配、过滤、分组、排序、聚会等多种业务运算
- 与java无缝集成,适用于jdk1.6及以上版本
- 支持new java对象,并可以使用Java所有类及对象
- 可以采用obj.field方式访问和操作对象属性,简化obj.getField()和obj.setField(value);
- 支持数据结构间相互转换
- 支持调用java非静态方法和静态方法
- 支持bean对象,可以操作bean对象的属性和方法
- 可以和Spring集成,方便加载bean配置信息
- 支持访问数据库,可以将表数据转换成序表结构
- 支持访问Excel,可以将Sheet数据转换成序表结构
- 支持访问文本,可以将行数据转换成序表结构
- 支持不同数据源的序表操作,比如关联、匹配等
- 支持object[key]扩展,比如访问list[1],map[key],简化用户操作
- 支持object.field扩展,允许用户实现不同语法场景
- 支持object.function(…)扩展,允许用户实现不同语法场景
- 支持java的基本类型,内置不同精度的数值转换函数
- 支持if/elseif/else、switch指令
- 支持for、while循环指令
- 支持基本表达式操作,符合java语法规范
- 允许用户设置下标是否从0开始,方便用户访问元素
- 支持[a .. b]方式生成指定范围的序列
- 允许用户定制常量,可以在脚本引擎构造后直接使用,无需声明,如PI、E等。
- 内置聚合函数和三角函数等系统函数,允许用户自行编写函数类进行扩展。
- 允许用户编写脚本类,简化业务逻辑。
- 允许用户编写脚本文件,同时支持java方式和IDE插件调用,实现即时开发测试。
- 支持动态更新脚本文件,无需重新编译部署
- 允许用户通过快速运行器执行脚本,也允许用户通过带Spring的运行器执行需要Spring环境的脚本
- 定义了基本操作符,但是允许用户配置不同的对象实现重载。
- 提供集合的差并交异或运算
- 允许对集合子元素进行批量操作符运算,返回新的集合,如list*2
- 允许对集合子元素进行批量方法运算,返回新的集合,如list.getName()
- 允许对集合子元素进行批量属性运算,返回新的集合,如list.age
- 支持lambda表达式,部分函数允许使用lambda表达式简化逻辑
- 增强lambda特性,允许lambda变量修改外部同名变量。
- 支持排列的lambda遍历操作
- 支持组合的lambda遍历操作
- 支持全排列的lambda遍历操作
- 支持单方法接口的lambda封装,如Runnable、Comparator
- 支持各种脚本内嵌执行,比如dataSource[[ sql语言 ]] 进行带@占位符的sql动态执行,支持template[[ 模板语言 ]] 进行模板语言执行,也可以继承各种其他脚本
当然上面列的不一定全,后面也会有新的语言特性加入。
脚本运行
脚本语言的扩展名是ts和tinyscript,当然也可以起其他的扩展名。
提供了Eclipse和Idea的执行器插件,安装之后可以右键直接运行脚本文件。
先推出看看反响如何,如果反响比较好,准备开发ide,支持高亮、调试等等。
配置Maven依赖
请在pom文件增加如下配置,注意tinyscript工程的版本一般选择最新正式版本
配置bean文件
本操作是可选项,如果使用者需要使用脚本(*.tinyscript)并且通过tiny文件扫描器加载,那么在bean文件配置如下信息:
scriptSegmentFileProcessor文件扫描器可以把脚本自动注册到脚本引擎。
Java方式运行
早期tinyscript没有提供IDE前端插件,只能通过Java接口调用验证
//不涉及调用脚本方法(无需注册脚本) ComputeEngine engine = new DefaultComputeEngine(); ScriptContext context = new DefaultScriptContext();
以上代码实例化脚本引擎和上下文环境,如果注册脚本有两种方式:一种是配置bean文件扫描器,另一种是手动注册到脚本引擎
//涉及调用脚本方法(手动注册脚本) ComputeEngine engine = new DefaultComputeEngine(); ScriptContext context = new DefaultScriptContext(); String content = FileUtil.readFileContent(new File("src/test/resources/multiresult.tinyscript"), "utf-8"); //本脚本路径仅做演示,用户需取实际地址 ScriptSegment scriptSegment = ScriptUtil.getDefault().createScriptSegment(engine, null, content); engine.addScriptSegment(scriptSegment);
如果执行上述代码没有抛出异常或输出异常日志,表示tinyscript初始化正常。
Eclipse插件
请参考:《Tiny模板运行器》,安装模板运行器插件到Eclipse上。该模板运行器也同样适用于脚本语言。
用户可以新建一个脚本文件,比如叫example.tinyscrpt 。请注意编码需要是utf-8,然后打开编辑器输入如下代码:
elements =[0,1,2,3,4,5,6,7,8,9]; elements.permute(3,(e) -> { value = e[1]*100+e[2]*10+e[3]; if(pow(e[1],3)+pow(e[2],3)+pow(e[3],3)==value){ System.out.println(value); } });
这段代码可以计算水仙花数,然后右键菜单"Run as"-"运行",命令窗口可以得到执行结果:
通过这种方式,用户可以快速编辑、测试脚本代码。
基本类型
脚本支持Java的7种基本类型,包括boolean、char、short、int、long、float、double。
boolean型
最为简单,仅有true/false真假值定义。
脚本片段如下:
//boolean类型 println(true); println(false);
执行结果:
true false
char型
仅支持单字符,与java规范相同,支持特殊字符定义如\t,\n
脚本片段如下:
//char类型 println('1'); //单字符数值 println('a'); //单字符字母 println("start"+'\t'+"end"); //特殊字符:制表符
执行结果:
1 a start end
int型
整型范围与java规范相同,支持二、八、十、十六进制的表示。+/-前缀表示正负数,正数可以省略前缀
脚本片段如下:
//int类型 println(123); //十进制的123 println(0110); //八进制,结果为72 println(0x6B7C); //十六进制,表示27516 println(0b111); //二进制,表示7 println(-123); //十进制的负数 println(-0110); //八进制的负数 println(-0x6B7C); //十六进制的负数 println(-0b111); //二进制的负数
执行结果:
123 72 27516 7 -123 -72 -27516 -7
long型
长整型范围与java规范相同,也支持二、八、十、十六进制的表示,但是需要用L/l做结尾。
脚本片段如下:
//long类型 println(99999999L); //十进制的99999999 println(0110L); //八进制,结果为72 println(0x6B7CL); //十六进制,表示27516 println(0b111L); //二进制,表示7 println(-99999999L); //十进制的负数 println(-0110L); //八进制的负数 println(-0x6B7CL); //十六进制的负数 println(-0b111L); //二进制的负数
执行结果:
99999999 72 27516 7 -99999999 -72 -27516 -7
float型
单精度浮点数遵守java规范,也是浮点数的默认类型。支持科学计数法。
脚本片段如下:
//float类型 println(1.1); //默认浮点类型 println(1.1f); //指定单精度类型 println(.1f); //小于1的浮点数可以省略0前缀 println(9f); println(1.0e3f); //科学计数法表示浮点 println(0x7.5p8f); //十六进制的浮点数 println(-2.3f); //负单精度浮点数,正单精度浮点数可以省略前缀+
执行结果:
1.1 1.1 0.1 9.0 1000.0 1872.0 -2.3
double型
双精度浮点数遵守java规范,结尾需要用D/d做结尾
脚本片段如下:
//double类型 println(2.53D); //双精度浮点数不能省略结尾D/d println(.53d); //小于1的浮点数可以省略0前缀 println(10d); println(1.0e3d); //科学计数法表示双精度浮点 println(-5.6d); //负双精度浮点数,正双精度浮点数可以省略前缀+
执行结果:
2.53 0.53 10.0 1000.0 -5.6
String型
String表示字符串,虽然不是基本类型,因为使用场景非常多,所以一并列举。语法使用双引号包含字符串,如果字符串本身包含",需要使用\进行转义
脚本片段如下:
//String类型 println("abc"); //一般字符串 println("if"); //包含指令关键字的字符串 println("null"); //包含null关键字的字符串 a="cat"; d="dog"; op="and"; //引擎支持字符串嵌套变量,减少用户使用+拼接字符串 println("dog and ${b}"); //字符串的$渲染,语法符合找不到对象b,返回空值 println("dog and ${a}"); //字符串的$渲染,渲染a对象 println("${a}{}[]"); //字符串的$渲染,渲染a对象 println("mmmmm${a}"); //字符串的$渲染,渲染a对象 println("dog and ${1+2+3}"); //字符串的$渲染,支持表达式 println("##${a} ${op} ${b}##"); //渲染包含多个变量
执行结果:
abc if null dog and dog and cat cat{}[] mmmmmcat dog and 6 ##cat and ##
null
空值,表示对象为空。注意null与"","null"是不一样的。
脚本片段如下:
//null println(a==null); //判断对象是否非空 println(b==null); //判断对象是否非空
执行结果:
false true
表达式运算
表达式由元素和操作符组成,元素一般可以分为基本类型和变量,基本类型如1,20000L,3.8d,false等,变量的话只是展示一个变量名,具体变量值存储在上下文。操作符种类很多如:四则运算操作符、逻辑运算符、移位符、三元表达式等等。本章节会逐一介绍。
1+2-3; //值运算 a+1-b; //变量表达式运算,如果上下文不存在a、b就会出错
实际使用场景基于变量的表达式运算最为常见
整型的四则运算
// 基本的加、减、乘、除、求模、括号 println(1+2+3); println(1+2*3); println(100-5-50+177); println(10000/4/100+4); println(2343%5); println(1/2); println(-1/2); println(3*(2+2)-7); println((2+5)*(6-3)-7*2);
整型的四则运算与java相同,特别是注意整型除法,不是四舍五入,需要注意。
执行结果:
6 7 222 29 3 0 0 5 7
浮点数的四则运算
// 浮点的加、减、乘、除 println(1.0f+2.0d); //结果3.0d println(1.0f-2.0d); //结果-1.0d println(1.0d*2.5d-1.0d); println(1.0d/2);
浮点的四则运算优先级与整型一样
执行结果:
3.0 -1.0 1.5 0.5
逻辑运算:与、或、非、异或
脚本片段如下:
// 与、或、非、异或 println(!false); println(!true); println(~2); println(128&129); println(128|129); println(15^2);
执行结果:
true false -3 128 129 13
逻辑运算:短路操作
执行脚本片段如下:
println(1=='1'); println(1==0 && 1==1); println(1=="1"); println(1==0 || 1==1); println(1==1 || 6>7 || 8!=8 ); println(1==1 && a!=null && a.length()>8); //测试与的短路操作 println(1==0 || a==null || a.length()>8); //测试或的短路操作
执行结果:
false false true true true false true
逻辑运算:大小比较
执行脚本片段如下:
println(1==1); println(1==0); println(1!=0); println(0!=0); println(200>199); println(200>=200); println(200<199); println(200<=200);
执行结果:
true false true false true true false true
位移操作
执行脚本片段如下:
println(8>>2); println(8<<2); println(-121 >>> 4);
执行结果:
2 32 268435448
三元表达式
执行脚本片段如下:
println(1+1>=1?"yes":"no"); println(2==5-3?true:false); println(2!=5-3?true:false); a=10;b=20; println(a==20?'a':(b==10?'b':'c')); a=10;b=10; println(a==20?'a':(b==10?'b':'c')); a=20;b=20; println(a==20?'a':(b==10?'b':'c'));
执行结果:
yes true false c b a
高级示例
背包问题
//参数说明:list.DPknapsack(容量,重量,[每件物品的件数],价值,[规则])class Obj{ name,weight,value; Obj(name,weight,value){ } } //==================================================================================================== //01背包list=[new Obj("a",2,6.0),new Obj("b",2,3.0),new Obj("c",6,5.0),new Obj("d",5,4.0),new Obj("e",4,6.0)]; println("01背包问题:\n"+list.dpKnapsack(10,list.weight,1,list.value)); //完全背包list=[new Obj("a",2,6.0),new Obj("b",2,3.0),new Obj("c",6,5.0),new Obj("d",5,4.0),new Obj("e",4,6.0)]; println("完全背包问题:\n"+list.dpKnapsack(10,list.weight,list.value)); //多重背包list=[new Obj("a",12,4.0),new Obj("b",2,2.0),new Obj("c",1,1.0),new Obj("d",4,10.0),new Obj("e",1,2.0)]; println("多重背包问题:\n"+list.dpKnapsack(15,list.weight,[1,7,12,3,1],list.value)); //混合背包list=[new Obj("a",12,4.0),new Obj("b",2,2.0),new Obj("c",1,1.0),new Obj("d",4,10.0),new Obj("e",1,2.0)]; println("多重背包问题:\n"+list.dpKnapsack(15,list.weight,[1,7,12,3,-1],list.value));
运行结果
01背包问题: [{result=15.0}, [Obj[name=a,weight=2,value=6.0], Obj[name=b,weight=2,value=3.0], Obj[name=e,weight=4,value=6.0]], [1, 1, 1]] 完全背包问题: [{result=30.0}, [Obj[name=a,weight=2,value=6.0]], [5]] 多重背包问题: [{result=34.0}, [Obj[name=b,weight=2,value=2.0], Obj[name=d,weight=4,value=10.0], Obj[name=e,weight=1,value=2.0]], [1, 3, 1]] 多重背包问题: [{result=36.0}, [Obj[name=d,weight=4,value=10.0], Obj[name=e,weight=1,value=2.0]], [3, 3]]
计算股票涨停
下面是某证券交易所一个月内的日收盘价记录,其中CODE列为股票代码,DT为日期,CL为收盘价。试找出这个月内曾连续三天涨停的股票。为避免四舍五入产生的误差,涨停的比率定为9.5%。
部分数据请见下图(完整记录请见附件stockRecords.txt,之后示例也遵守此规范)
编写example1.tinyscript如下:
import org.tinygroup.etl.DataSet; import org.tinygroup.etl.Field; class Example1 { /* 统计一个月内连续三天涨停的股票 */ countStock(path) { ratio = 0.095d; ds = readTxt(path); groupds =ds.insertColumn(3,"UP").convert(CL,"double").group(CODE).sortGroup("DT ASC"); groupds.subGroup(1,1).update(UP,0d); //每月的第一天涨停率为0 groupds.update(UP,(CL[0]-CL[-1])/CL[-1]); //之后的每天统计当天的涨停率。 resultds = groupds.filterGroup(UP[0]>ratio && UP[1]>ratio && UP[2]>ratio); return resultds; } }
调用脚本的java代码:
public void testWithScript() throws Exception{ System.out.println("testWithScript is start!"); GroupDataSet groupDs = (GroupDataSet) engine.execute("m = new Example1(); return m.countStock(\"src/test/resources/StockRecords.txt\");", context); //输出结果股票 for(int i=0;i<groupDs.getRows();i++){ System.out.println("code="+groupDs.getData(i+1,1)); } System.out.println("testWithScript is end!"); }
结果如下:
testWithScript is start! code=201745 code=550766 code=600045 code=700071 testWithScript is end!
下面是某证券交易所一个月内的日收盘价记录,其中CODE列为股票代码,DT为日期,CL为收盘价。试找出这个月内曾连续三天涨停的股票。为避免四舍五入产生的误差,涨停的比率定为9.5%。
部分数据请见下图(完整记录请见附件stockRecords.txt,之后示例也遵守此规范)
编写example1.tinyscript如下:
import org.tinygroup.etl.DataSet; import org.tinygroup.etl.Field; class Example1 { /* 统计一个月内连续三天涨停的股票 */ countStock(path) { ratio = 0.095d; ds = readTxt(path); groupds =ds.insertColumn(3,"UP").convert(CL,"double").group(CODE).sortGroup("DT ASC"); groupds.subGroup(1,1).update(UP,0d); //每月的第一天涨停率为0 groupds.update(UP,(CL[0]-CL[-1])/CL[-1]); //之后的每天统计当天的涨停率。 resultds = groupds.filterGroup(UP[0]>ratio && UP[1]>ratio && UP[2]>ratio); return resultds; } }
调用脚本的java代码:
public void testWithScript() throws Exception{ System.out.println("testWithScript is start!"); GroupDataSet groupDs = (GroupDataSet) engine.execute("m = new Example1(); return m.countStock(\"src/test/resources/StockRecords.txt\");", context); //输出结果股票 for(int i=0;i<groupDs.getRows();i++){ System.out.println("code="+groupDs.getData(i+1,1)); } System.out.println("testWithScript is end!"); }
结果如下:
testWithScript is start! code=201745 code=550766 code=600045 code=700071 testWithScript is end!
演示排序、组合和全排序
TinyScript提供了3种有关排列组合的函数,分别是permute(排列),combine(组合)和permuteAll(全排列)。让用户在有关数学运算方面使用起来更加方便。下面是这三个函数的有关使用的样例Permute:遍历数组所有数字的排列组合,每位数字都可重复。
使用方式: Permute(num,lambda)其中第一个参数代表输出排列数字的位数,第二个参数代表处理的匿名lambda表达式。也可以采用Permute(lambda)的形式,此时num默认为集合的数据个数。
样例:
elements = [0,1,2]; elements.permute(3,(record) -> { println(record); });
elements = [0,1,2]; elements.permute((record)->{ println(record); });
来看一个更复杂的水仙花数的计算:
elements =[0,1,2,3,4,5,6,7,8,9]; elements.permute(3,(e) -> { value = e[1]*100+e[2]*10+e[3]; if(pow(e[1],3)+pow(e[2],3)+pow(e[3],3)==value){ System.out.println(value); } });
permuteAll:该函数输出数组的全排列。数字不会重复。
使用方式:permuteAll(num,lambda),num表示全排列的数字个数,lambda是处理排列后数字的匿名函数。可以使用permuteAll(lambda),此时num默认为集合的数据个数。
样例:
elements = [0,1,2]; elements.permuteAll(2,(record)->{ println(record); });
elements = [0,1,2]; elements.permuteAll((record) -> { println(record); });
Combine:该函数输出数组的所有组合。
使用方式:combine(num,lambda),第一个参数num代表组合的位数,第二个参数代表处理每个组合数字的函数。也可以使用combine(lambda),此时num默认为集合的数据个数。
样例:
输出数组中3位一组合的所有组合形式:
elements = [0,1,2]; elements.combine(2,(record)->{ println(record); });
elements = [0,1,2]; elements.combine((record)->{ println(record); });
此外,还可以嵌套使用,比如我先求一个数组的所有组合再对组合结果进行全排列:
elements = [1,2,3,4]; elements.combine((record) -> { if(record.size()==3){ record.permuteAll((e)->{ println(e); }); } });
序列的差并交异或
TinyScript支持对序列之间进行差并交异或的运算,具体使用方式如下。
序列的差(减去相同元素,若b在前面则结果是4):
a=[1,2,3]; b=[1,2,4]; println(a-b);
或者用内置函数subtract,形如
a.subtract(b)
序列的相对差(交集的补集也就是异或):
a=[1,2,3]; b=[1,2,4]; println(a^b);
或者用内置函数xor,形如
println(a.xor(b));
序列的并集:
a=[1,2,3]; b=[1,2,4]; println(a+b);
或者用内置函数unite,例如:
println(a.unite(b));
序列的交集:
a=[1,2,3]; b=[1,2,4]; println(a&b);
或者用内置函数intersect,例如:
println(a.intersect(b));
理财产品优化组合
class Product{ name,amountPerServing ,maxCount,rate; //name:基金名字 amountPerServing:一份的价格 maxCount:最大份数 rate:利率Product(name,amountPerServing,maxCount,rate){ } } //============================================================================days=80; list=[ new Product("鹏华国防",100,10,0.00045), new Product("鹏华中证",100,20,0.00035), new Product("国投瑞银",100,20,0.00055), new Product("华商主题精选",100,10,0.0004), new Product("金鹰智慧",100,5,0.0003)]; println(list.dpKnapsack(5000,list.amountPerServing,list.maxCount,()->{ return [0.00045,0.00035,0.00055,0.00040,0.00030]*days*100;//每个基金的总收益由利率*时间*一份价格算出}));
运行结果:
[ {result=183.999998}, [Product[rate=4.5E-4,name=鹏华国防,maxCount=10,amountPerServing=100], Product[rate=3.5E-4,name=鹏华中证,maxCount=20,amountPerServing=100], Product[rate=5.5E-4,name=国投瑞银,maxCount=20,amountPerServing=100], Product[rate=4.0E-4,name=华商主题精选,maxCount=10,amountPerServing=100]], [10, 10, 20, 10] ]