{"id":19151765,"url":"https://github.com/zhs007/ds-lang","last_synced_at":"2026-02-19T01:02:03.421Z","repository":{"id":57216636,"uuid":"45654012","full_name":"zhs007/ds-lang","owner":"zhs007","description":"开源的数据建模语言","archived":false,"fork":false,"pushed_at":"2016-01-30T14:08:40.000Z","size":86,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-09-24T05:21:41.289Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zhs007.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-11-06T02:26:35.000Z","updated_at":"2025-09-21T16:25:23.000Z","dependencies_parsed_at":"2022-08-26T13:31:25.758Z","dependency_job_id":null,"html_url":"https://github.com/zhs007/ds-lang","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/zhs007/ds-lang","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhs007%2Fds-lang","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhs007%2Fds-lang/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhs007%2Fds-lang/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhs007%2Fds-lang/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zhs007","download_url":"https://codeload.github.com/zhs007/ds-lang/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zhs007%2Fds-lang/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29600352,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T00:59:38.239Z","status":"ssl_error","status_checked_at":"2026-02-19T00:59:36.936Z","response_time":162,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-09T08:15:39.796Z","updated_at":"2026-02-19T01:02:03.389Z","avatar_url":"https://github.com/zhs007.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ds-lang\n数据建模语言。\n\n主要用于我们自己的工作流。\n\n工作流\n---\n* **建模语言 [ds-lang](https://github.com/zhs007/ds-lang)** - 建模后，可以生成sql脚本、excel配置文件、protobuf文件。\n* **excel配置文件导出工具 [xlsx2csv](https://github.com/zhs007/xlsx2csv)** - 填好excel配置文件后，我们可以将excel文件导出到csv，配合生成的代码，实现excel文件自动展开到内存中。\n\nwhy\n---\n对于程序员来说，数据建模，最方便的还是写代码，定义一个struct，是最简单直接的了，而写sql语句之类的，甚至操作phpmyadmin，都是一件很麻烦的事情，更何况，操作完phpmyadmin以后，还是要写一遍struct，甚至还要写中间代码，这部分工作几乎是重复劳动。\n\n为什么不能写一个struct，就把数据库建好，然后把各种代码自动生成出来呢？\n\n不管是mysql，还是redis，甚至是既有mysql持久化又有redis缓存，其实数据建模本身几乎是无差异的。\n\n上面就是这个建模语言的初衷。\n\n其实有了建模数据以后，我们还可以用这个数据做很多自动化编码的工作，省时省事而且降低人为编码的错误率。\n\n进一步来说，我们还会有一系列的工具，做更多的自动化可视化流程，可以影响到策划、美术、测试、运营等等。\n\n我们需要的只是一些更好读的中间数据，而ds-lang做的仅仅是更方便的产生这一批好读的中间数据。\n\n更新说明\n---\n* **0.6.6**\n * 增加 **float** 的支持。但是，考虑到浮点数的精确性问题，不建议使用浮点数，不到必要时刻，建议使用整数最后程序里面作为万分数来使用。\n\n* **0.6.3**\n * 增加 **--ver** 参数，查看版本号。\n * 增加对文件是否存在的检查。\n\n* **0.6.2**\n * 增加 **VER** 的宏，必须要声明，采用 **1512280** 的格式，其中前面6位是年月日，最后一位是今日的版本号，也就是一天最多只能有10个有效版本。\n * **excel导出** 时，会兼容老版本。\n * 增加 **DELETE** 关键字，前置删除属性用。\n\n* **0.6.1**\n * 导出的**excel**表格里，会在**tab页**里加入**枚举表**，方便编辑查阅。\n\n* **0.6.0**\n * 将内核部分独立成一个新项目 **[dsl-core](https://github.com/zhs007/dsl-core)** 。\n\n* **0.5.2**\n * 使用**[handlebars](https://github.com/wycats/handlebars.js/)**重构代码生成部分。\n * 支持crystal导出NodeJS服务端代码。\n * 支持枚举直接做默认值。\n * 修正展开子结构时没有考虑下划线的bug。\n\n* **0.5.1**\n * 代码生成时，对齐代码。\n * 调整了代码生成器插件的部分接口。\n * 支持crystal框架的代码生成。\n * 代码生成插件支持多文件的生成。\n * 支持crystal导出C++客户端代码。\n\n* **0.5.0**\n * 加入代码生成功能。\n * 规范了后续代码生成插件的制作方式。\n\n* **0.3.2**\n * 对重名做了更严格的检查，下划线开头不能算一个单独的名字。\n * 增加 **map** 关键字，主要用于数据管理层。\n\n* **0.3.1**\n * 对消息协议进一步支持。\n * 支持导出**protobuf**。\n * 增加关键字**bytes**，表示二进制数据流类型。\n * 增加关键字**bool**，表示bool类型。\n * 增加关键字**true**和**false**。\n\n* **0.3.0**\n * 增加**message**关键字，用来处理消息协议。\n * 增加**struct**名字前加下划线，不单独为数据库建表的约定。\n * 增加对普通结构体成员**expand**的语法支持，就是直接展开子结构体，而少一层查找关系，一定程度上可以用来实现类继承的设计。\n * 增加设置**AUTOINC**的初值。\n\n ```\n\tprimary PlayerID pid = AUTOINC(10000);\t\t// 角色唯一标识，10000开始\n``` \n\n* **0.2.5**\n * 调整解析顺序，改成和输入顺序一致。\n * 修正输出excel文件时重复创建目录失败的bug。\n * 增加对静态表枚举展开(**expand**)的支持（暂不支持动态表的枚举展开）。\n * 增加结构内数组主索引字段的语法支持。\n\n ```\nrepeated PlayerGameInfo(pid) _gameinfo;\t// 游戏信息\n```\n\n * 修正小bug若干。\n\n* **0.2.4**\n * 增强语法检验。\n * 增加命令行参数**--sql**，能直接输出sql语句。\n * 增加命令行参数**--excel**，能将**static**表导出成excel文件。\n\n* **0.2.3**\n * 修正npm发布项目的bug，**package.json**文件需要**latest**属性。\n\n* **0.2.2**\n * 修正命令行工具的bug。\n\n* **0.2.1**\n * 增加 NULL 关键字，用于结构属性，表示该属性可以没有值。\n * 明确了基本默认值。\n\n* **0.2.0**\n * 增加对结构声明的语法支持。\n\n ```\nstruct MAGInfo;\t\t// MAG磁力下载站\n```\n\n * 完善注释的解析，去掉了前后无意义字符（whitespace）。\n * 增加命名规范的支持。\n * 增加 unique 关键字，用于唯一且不是主键的索引。\n * 增加 AUTOINC 关键字，用于结构中，主键的默认值。\n * 增加 NOW 关键字，用于结构中，时间属性的默认值。\n\n* **0.1.0**\n * 制定基本的语法规则。\n * 解析器。\n\n语法\n---\n第一次构建一种语言，非常的没有经验，基本上是按照类C语法做的，部分参考google的protobuf，在使用方面，更多的借鉴动态语言，譬如结构默认值就是直接写在结构里了。\n\n原则上，是**强类型**、**强注释**、**强编码规范**的，语法符合大部分人的编码习惯，不反人类。\n\n* **强类型** - 每个对象或变量都是先给定类型的，也鼓励大家用typedef定义新的类型，譬如把ID和int分开（其实本质上ID也就是int）。\n\n ```\ntypedef int ID;\t// 定义ID类型，实际上是int\n```\n\n* **强注释** - 必须有注释，否则编译不过。因为这是一个建模语言，基本上每个结构每个属性都应该有注释，一定要让人看明白。\n\n* **强编码规范** - 在语言层面强化编码规范，譬如golang这样的，我个人是认同这个方案的。一些最基本的东西需要遵循一定的规范，遵循规范的同时，其实可以带来一些编码的畅快感（少输入）。\n\n基本语法：\n\n* **typedef** - 类型重定向，也就是C里面的typedef。\n* **struct** - 结构体，类似C的struct定义，唯一的差别就是支持默认值，直接在声明里写就可以了。\n* **static** - 静态结构体定义，类struct，只是明确的表示，这是一个静态配置表。\n* **enum** - 枚举，枚举其实是一组常量的集合，语法上和C有些不同。枚举必须大写，而且枚举内容必须是大写枚举加下划线开头，而且枚举是以分号分隔的，每一行定义必须显示的写好对应int值。枚举的值是可以在后面直接使用的。\n\n ```\n// 英雄属性枚举\nenum HEROATTR{\n\tHEROATTR_VIT = 0;\t// 体力 - vitality\n\tHEROATTR_STA = 1;\t// 耐力 - stamina\n\tHEROATTR_STR = 2;\t// 力量 - strength\n\tHEROATTR_INT = 3;\t// 智力 - intelligence\n\tHEROATTR_DEX = 4;\t// 敏捷 - dexterity\n\tHEROATTR_CRIT = 5;\t// 暴击 - critical\t\t\n\tHEROATTR_PARR = 6;\t// 格挡 - parry\n\tHEROATTR_HIT = 7;\t// 命中 - hit\n\tHEROATTR_MISS = 8;\t// 闪避 - miss\n};\n```\n\n* **全局变量** - 任何定义在结构体和静态表之外的单独变量都是全局变量。全局变量必须由大写字母、下划线、数字组成，且必须以大写字母打头。\n* **结构声明** - 可以在结构体定义以前，声明一个结构体，主要用于A依赖B，B依赖A的情况。\n\n ```\nstruct MAGInfo;\t\t// MAG磁力下载站\n```\n\n \u003e **注意：**结构声明的注释如果和后面定义的不一样，会拿定义的注释覆盖注释。\n\n* **结构属性前缀** - 对于结构体来说，有一组简单的前缀，可以方便后续工作流的更好工作。\n * **primary** - 主键，也就是这个结构体的唯一标识，如果最后会选择sql数据库落地的话，这个也就是数据库的主键了。\n * **联合主键** - primary0、primary1，也就是多个主键一起构成联合主键。\n * **index** - 索引，逻辑上，可能会需要根据该属性做查找工作，在数据库里也就是要为他建索引。\n * **unique** - 唯一的索引，是一个全局唯一的索引。\n * **枚举展开** - 会有很多时候，我们需要一个特定意义的数组，每个单元是一个特殊含义（也就是有一个枚举做数组下标）。我们可以通过一个简单的前缀做到这点。\n\n ```\nexpand(HEROATTR) int heroattr;\t\t// 英雄属性数组，根据HEROATTR展开\n```\n\n * **数组** - 类似protobuf的数组。\n\n ```\nrepeated HeroInfo lsthero;\t\t\t// 英雄列表\n```\n\n * **单根树状数组** - 在游戏里，存在着大量的单根树状结构，譬如一个角色带着一堆宝宝，每个宝宝身上都还有一堆装备，每件装备上还有一堆宝石等，这种单根树状结构会自动被正确识别上层主键加载。\n\n ```\n\trepeated PlayerGameInfo(pid) _gameinfo;\t// 游戏信息\n```\n\n* **结构属性默认值** - 结构属性可以给一个默认值。\n * **基本默认值** - 基本类型都有一个基本的默认值，如果整数类的，默认值是0，字符串则是一个空字符串。\n * **常量默认值** - 就是一个常量，或者前面已经定义好的枚举值，如果是数值型的，可以有四则运算。\n * **默认没有值** - 主要也是数据库用的，默认该属性没有值，用 NULL 来表示。当前版本下，内存中是一定有值的，也就是基本默认值。\n * **自增长整数** - 数据库常用的，我们可以用 AUTOINC 来表示。这个只能用于主键。\n\n ```\n\tprimary PlayerID pid = AUTOINC;\t\t// 角色唯一标识\n``` \n\n * **当前时间** - 也是数据库常用的，我们可以用 NOW 来表示。一般来说，数据库只允许一列用这个属性，而我们不存在这种要求。\n\n ```\n\ttime regtime = NOW;\t\t\t\t\t// 注册时间\n\ttime lastlogintime = NOW;\t\t\t// 最后一次登录时间\n```\n\n* **通信协议** - 通信协议是很重要的一部分内容，通信协议是message，成员变量的定义方式基本上和struct一样。\n * **通信协议的基本概念** - 一般来说，通信协议分为2部分，一个是通信协议标识，我们定义为MsgID，另外一个是通信协议结构定义，也就是我们的message定义。\n * **自动生成MsgID** - 只要按照我们的命名规范来定义协议，我们就可以自动生成MsgID，并保证全局唯一易于维护更新方便。\n * **协议的命名** - 客户端发出的协议必须以 **Req_** 开头，服务器发出的协议必须以 **Res_** 开头。这样命名以后，会自动生成2组MsgID。\n * **控制MsgID的区间** - 我们可以重载全局变量 **REQ_MSGID_BEGIN** 和 **RES_MSGID_BEGIN** 来重新定义服务器和客户端的消息起点，默认是10000和20000开始排列。\n * **重载MsgID类型** - 默认的**MsgID**是**int**的，我们也可以重载**MsgID**类型。\n\n* **命名规范** - 命名有一定的要求。\n * **枚举类型命名** - 枚举名只能是大写字母和下划线，且不能以下划线开头。\n * **枚举值命名** - 枚举值一定是枚举类型名加下划线开头。\n * **类型命名** - 类型命名一定是大写字母开头。\n * **类型属性命名** - 类型属性一定是小写字母或者下划线开头。\n * **客户端不需要看到的属性** - 类型属性中，如果有部分属性是客户端不需要看到的，下划线开头。\n * **全局变量命名** - 全局变量一定由大写字母和下划线组成，且不能由下划线开头。\n * **下划线开头的名字** - 下划线开头的名字有特殊的意义，因此2个名字如果一个下划线开头，一个没有下划线开头的，算重名处理。\n\n基本变量类型：\n\n* **int** - 整数，32位整数，有符号\n* **int64** - 64位整数，有符号\n* **time** - 时间戳，32位，最大到2038年\n* **float** - 浮点数，32位\n* **string** - 字符串，一般来说，不建议超过256字符\n* **info** - 长字符串\n* **bytes** - 二进制数据流\n* **bool** - bool类型，只有2个值，就是 **true** 和 **false**\n\n例子：\n\n```\n\nint MAX_LEVEL = 80;\t\t\t// 最大等级\n\ntypedef int HeroExpType;\t// 英雄经验类型\n\n// 英雄属性枚举，枚举必须大写，而且枚举内容必须是大写枚举加下划线开头\nenum HEROATTR{\n\tHEROATTR_VIT = 0;\t// 体力 - vitality\n\tHEROATTR_STA = 1;\t// 耐力 - stamina\n\tHEROATTR_STR = 2;\t// 力量 - strength\n\tHEROATTR_INT = 3;\t// 智力 - intelligence\n\tHEROATTR_DEX = 4;\t// 敏捷 - dexterity\n\tHEROATTR_CRIT = 5;\t// 暴击 - critical\t\t\n\tHEROATTR_PARR = 6;\t// 格挡 - parry\n\tHEROATTR_HIT = 7;\t// 命中 - hit\n\tHEROATTR_MISS = 8;\t// 闪避 - miss\n};\n\n// 英雄技能枚举\nenum HEROSKILL{\n\tHEROSKILL_SKILLID1 = 0;\t\t// 普通技能1\n\tHEROSKILL_SKILLID2 = 1;\t\t// 普通技能2\n\tHEROSKILL_SKILLID3 = 2;\t\t// 普通技能3\n\t\n\tHEROSKILL_BSKILLID1 = 3;\t// 条件技能1\n\tHEROSKILL_BSKILLID2 = 4;\t// 条件技能2\n\t\n\tHEROSKILL_MSKILLID1 = 5;\t// 主动技能1\n};\n\n// 玩家经验表\nstatic PlayerExp{\n\tprimary int playerlevel;\t// 玩家等级\n\t\n\tindex int totalplayerexp;\t// 玩家当前等级需要的总经验（不算当前等级需要的经验）\n\t\n\tint playerexp;\t\t\t\t// 玩家当前等级升级需要的经验\n};\n\n// 英雄经验表\nstatic HeroExp{\n\tprimary0 HeroExpType heroexptype;\t// 英雄经验类型\n\tprimary1 int herolevel;\t\t\t\t// 英雄等级\n\t\n\tindex int totalheroexp;\t\t\t\t// 英雄当前等级需要的总经验（不算当前等级需要的经验）\n\t\n\tint heroexp;\t\t\t\t\t\t// 英雄当前等级升级需要的经验\n};\n\n// 英雄基本配置表\nstatic HeroBase{\n\tprimary int heroid;\t\t\t\t\t// 英雄唯一标识\n\t\n\tHeroExpType heroexptype;\t\t\t// 英雄经验类型\n\t\n\texpand(HEROATTR) int heroattr;\t\t// 英雄属性数组，根据HEROATTR展开\n\texpand(HEROSKILL) int heroskill;\t// 英雄属性数组，根据HEROSKILL展开\n};\n\n// 英雄装备信息\nstruct HeroEquInfo{\n\tprimary int equid;\t\t\t\t\t// 装备唯一标识\n};\n\n// 英雄信息\nstruct HeroInfo{\n\tprimary int heroid;\t\t\t\t\t// 英雄唯一标识\n\t\n\tint herolevel;\t\t\t\t\t\t// 英雄等级\n\tint heroexp;\t\t\t\t\t\t// 英雄当前经验\n\t\n\texpand(HEROATTR) int heroattr;\t\t// 英雄属性数组，根据HEROATTR展开\n\texpand(HEROSKILL) int heroskill;\t// 英雄属性数组，根据HEROSKILL展开\n\t\n\trepeated HeroEquInfo lstequ;\t\t// 英雄列表\n};\n\n// 角色基本信息\nstruct PlayerInfo{\n\tprimary int pid;\t\t\t\t\t// 角色唯一标识\n\t\n\tstring name;\t\t\t\t\t\t// 角色名\n\ttime regtime;\t\t\t\t\t\t// 注册时间\n\ttime lastlogintime;\t\t\t\t\t// 最后一次登录时间\n\t\n\tint playerlevel;\t\t\t\t\t// 角色等级\n\tint playerexp;\t\t\t\t\t\t// 角色经验\n\t\n\tint gold;\t\t\t\t\t\t\t// 金币\n\tint gem;\t\t\t\t\t\t\t// 钻石\t\n\t\n\trepeated HeroInfo lsthero;\t\t\t// 英雄列表\n};\n\n```\n\n数据类型\n---\n由于ds-lang支持**typedef**，所以可以根据类型建立起数据之间的联系。\n\n为了更方便的自动建立各模块之间的联系，多用**typedef**吧。\n\n```\ntypedef int PlayerID;\t\t// 玩家ID类型\ntypedef int RoomID;\t\t\t// 房间ID类型\ntypedef int Gem;\t\t\t// 钻石类型\ntypedef int Gold;\t\t\t// 金币类型\ntypedef int PlayerLevel;\t// 玩家等级类型\ntypedef int PlayerExp;\t\t// 玩家经验类型\n```\n\n自动生成代码\n---\n我们的自动代码生成是插件方式的，其实就是根据生成的json文件来做代码生成。这里我们提供了我们项目的几种方式，你也可以根据你自己项目需要来实现适合你们项目的代码生成插件。\n\n* **HeyServ 框架** - 服务器是基于**PHP**的，**HTTP**短连接，客户端有一套**Cocos2d**的实现，包含**c++**和**lua**。\n* **Crystal 框架** - 服务器基于**Nodejs**的分布式服务器框架，**WebSocket**长连接，客户端也有一套**Cocos2d**的实现，包含**c++**和**lua**。\n\n数据存储级别\n---\n我们定义数据存储级别为```DB -\u003e CACHE -\u003e SERVICE -\u003e CLIENT```4个级别，我们认为SERVICE的数据是最完整的。\n\n下划线打头的结构成员是不同步到客户端的。\n\n下划线打头的结构体是不生成独立的表或记录的。\n\n自动的ID排布和版本管理\n---\nds-lang为了简化编码，大量的使用了自动的ID排布，譬如message和MSGID的自动生成。这样自动的ID排布，最大的问题就在版本同步上了，那我们应该怎样做版本管理呢？\n\n前面也说过ds-lang只是我们工作流的一部分，我们有一整套提交比对机制，保证版本同步，后续的工具也会逐步开放给大家的，譬如修改了数据建模，会自动生成差异化的sql脚本，供你同步sql数据库，也会主动修改excel文件内容，增减列。\n\n\u003e 注：理论上我们是不建议做删除数据的操作的，不能确定这一定是个合适的操作，所以后续版本会增加 DELETE 前缀。还会提供一个强制清理的命令，一次性清理所有不要的数据。\n\njison\n---\ngrammar目录内的dsl.jison文件就是jison的语法文件，可以通过命令行命令生成出dsl.js文件。\n\n```\njison dsl.jison\n```\n\n但是，为了能多次使用解释器，我们还加了一个初始化接口，最好在dsl.js文件里面显示的调用一下。\n\n```\nparse: function parse(input) {\n    __onInit();\n```\n\n至于jison的安装，可以参考[jison](https://github.com/zaach/jison)相关文档。\n\n如果对jison语法有兴趣，也可以看看我的另外一个项目[jison-demo](https://github.com/zhs007/jison-demo)，这里面有一些基础的例子，基本上ds-lang就是基于这些例子完善起来的。\n\n二次开发\n---\nds-lang的二次开发有2种方式：\n\n1. ds-lang是用jison生成的建模语言，相关的命令行工具和语法文件都是开源的，可以直接修改工具和语法文件获得新的支持。\n2. ds-lang会生成一个json格式文件，这个文件其实是非常好读的，也可以基于这个输出文件做后续的二次开发。\n\n如果你有更好的想法，也可以和我们联系。\n\n使用到的第三方库\n---\n\n* 使用**[jison](https://github.com/zaach/jison)**做语法分析。\n* 使用**[yargs](https://github.com/bcoe/yargs)**模块简化命令行工具的开发。\n* 使用**[node-xlsx](https://github.com/mgcrea/node-xlsx)**模块生成xlsx文件。\n* 使用**[handlebars](https://github.com/wycats/handlebars.js/)**模板做基本代码模板。","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzhs007%2Fds-lang","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzhs007%2Fds-lang","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzhs007%2Fds-lang/lists"}