2. 实现JSON 数字的解析。
2.1 JSON number 的语法规则与解释。
JSON number 的语法规则是这样的:
/*
number 以十进制表示,它主要由 4 部分组成:负号、整数、小数、指数。只有整数是必需部分。注意:+号是不合法的。
int 整数部分如果是 0 开始,只能是单个 0;而由 1-9 开始的话,可以加任意数量的数字(0-9)。也就是说,0123 不是一个合法的 JSON 数字。
frac 小数部分比较直观,就是小数点后是一或多个数字(0-9)。
JSON 可使用科学记数法,exp 指数部分由大写 E 或小写 e 开始,然后可有正负号,之后是一或多个数字(0-9)。
*/
JSON-text = ws value ws
ws = *(%x20 / %x09 / %x0A / %x0D)
value = number
number = [ "-" ] int [ frac ] [ exp ]
int = "0" / digit1-9 *digit
frac = "." 1*digit
exp = ("e" / "E") ["-" / "+"] 1*digit
JSON 标准 ECMA-404 采用图的形式表示语法,也可以更直观地看到解析时可能经过的路径:
2.2 设计头文件
JSON 是一个树形结构,我们设定每个节点使用结构体 lept_value表示,对于null、false、true 在解析后,我们只需要存储它的数据类型为LEPT_NULL、LEPT_FALSE、LEPT_TRUE;但对于数字,不仅要存储结点的类型,还要存储数据本身。
从 JSON 数字的语法,我们可能直观地会认为它应该表示为一个浮点数,因为它带有小数和指数部分。然而,标准中并没有限制数字的范围或精度。为简单起见,选择以双精度浮点数来存储 JSON 数字。
所以,此时 lept_value 组成就为:
typedef struct {
double n; //用来存储数值
lept_type type;
}lept_value;
API设计
/*
函数目的:获取JSON结点里的数值
参数:1. lept_value* v :传入存储解析后JSON树形结构的根节点指针
返回值:double
*/
double lept_get_number(const lept_value* v) ;
API 实现
/*
函数目的:获取JSON结点里的数值
参数:1. lept_value* v :传入存储解析后JSON树形结构的根节点指针
返回值:double
实现思路:仅当 type == LEPT_NUMBER时,才返回数值
*/
double lept_get_number(const lept_value* v) {
assert(v != NULL && v->type == LEPT_NUMBER);
return v->n;
}
2.3 TDD设计理念
先写测试函数,写测试函数时考虑的情况越多,最后在设计解析器的时候就越完善。
#define EXPECT_EQ_DOUBLE(expect, actual) EXPECT_EQ_BASE((expect) == (actual),expect,actual, "%.17g")
#define TEST_NUMBER(expect, json)\
do {\
lept_value v;\
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, json));\
EXPECT_EQ_INT(LEPT_NUMBER, lept_get_type(&v));\
EXPECT_EQ_DOUBLE(expect, lept_get_number(&v));\
} while(0)
static void test_parse_number() {
TEST_NUMBER(0.0, "0");
TEST_NUMBER(0.0, "-0");
TEST_NUMBER(0.0, "-0.0");
TEST_NUMBER(1.0, "1");
TEST_NUMBER(-1.0, "-1");
TEST_NUMBER(1.5, "1.5");
TEST_NUMBER(-1.5, "-1.5");
TEST_NUMBER(3.1416, "3.1416");
TEST_NUMBER(1E10, "1E10");
TEST_NUMBER(1e10, "1e10");
TEST_NUMBER(1E+10, "1E+10");
TEST_NUMBER(1E-10, "1E-10");
TEST_NUMBER(-1E10, "-1E10");
TEST_NUMBER(-1e10, "-1e10");
TEST_NUMBER(-1E+10, "-1E+10");
TEST_NUMBER(-1E-10, "-1E-10");
TEST_NUMBER(1.234E+10, "1.234E+10");
TEST_NUMBER(1.234E-10, "1.234E-10");
TEST_NUMBER(0.0, "1e-10000"); /* 必须下溢 */
//边界值测试
TEST_NUMBER(1.0000000000000002, "1.0000000000000002"); /* the smallest number > 1 */
TEST_NUMBER( 4.9406564584124654e-324, "4.9406564584124654e-324"); /* minimum denormal */
TEST_NUMBER(-4.9406564584124654e-324, "-4.9406564584124654e-324");
TEST_NUMBER( 2.2250738585072009e-308, "2.2250738585072009e-308"); /* Max subnormal double */
TEST_NUMBER(-2.2250738585072009e-308, "-2.2250738585072009e-308");
TEST_NUMBER( 2.2250738585072014e-308, "2.2250738585072014e-308"); /* Min normal positive double */
TEST_NUMBER(-2.2250738585072014e-308, "-2.2250738585072014e-308");
TEST_NUMBER( 1.7976931348623157e+308, "1.7976931348623157e+308"); /* Max double */
TEST_NUMBER(-1.7976931348623157e+308, "-1.7976931348623157e+308");
}
除了这些上面这些合法的测试用例,根据JSON数字解析规则我们来设计一些不合语法的用例:
#define TEST_ERROR(error, json, lept_type)\
do{\
lept_value v; \
v.type = LEPT_FALSE; \
EXPECT_EQ_INT(error, lept_parse(&v, json)); \
EXPECT_EQ_INT(lept_type, lept_get_type(&v)); \
} while (0);
static void test_parse_invalid_value() {
/* ... */
/* invalid number */
TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "+0");
TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "+1");
TEST_ERROR(LEPT_PARSE_INVALID_VALUE, ".123"); /* at least one digit before '.' */
TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "1."); /* at least one digit after '.' */
TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "INF");
TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "inf");
TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "NAN");
TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "nan");
TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "e1");
TEST_ERROR(LEPT_PARSE_INVALID_VALUE, "E1");
}
static void test_parse_root_not_singular() {
TEST_ERROR(LEPT_PARSE_ROOT_NOT_SINGULAR, "null x");
/* invalid number */
TEST_ERROR(LEPT_PARSE_ROOT_NOT_SINGULAR, "0123"); /* after zero should be '.' or
nothing */
TEST_ERROR(LEPT_PARSE_ROOT_NOT_SINGULAR, "0x0");
TEST_ERROR(LEPT_PARSE_ROOT_NOT_SINGULAR, "0x123");
}
static void test_parse_number_too_big() {
TEST_ERROR(LEPT_PARSE_NUMBER_TOO_BIG, "1e309");
TEST_ERROR(LEPT_PARSE_NUMBER_TOO_BIG, "-1e309");
}
拓展,double类型的数据如何设计边界值测试案例?
拓展链接:http://m.elecfans.com/article/614555.html
浮点数占64bit(8字节),其中符号位占1位,小数点前占11位,小数点后占52位(6.5个字节,可以存13个16进制数)。
11位可以取值1~ 2046,但是因为有符号的存在,实际存储范围-1022~1023
浮点数的整数部分全为0或全为1有特殊含义。
整数部分全为1,小数部分全为0表示无穷大,即inf。根据符号位的不同可以分为+inf / -inf
整数部分全为1,小数部分不全为0表示Nan。即not a number。
小数部分最高位为1的nan称为Qnan;最高位为0的nan称为Snan。通常用QNan表示不确定的操作,Snan表示无效的操作。
浮点数0000 0000 0000 0001(十六进制表示法)的小数部分(后13位)是0 0000 0000 0001,即1/252, 对应的数是1/(252) *2(-1022),即4.9406564584124654e-324
2.4 实现JSON_Number解析器
根据 API 与 单元测试 ,我们来实现解析器,首先是lept_parse函数
static int lept_parse_value(lept_context* c, lept_value* v) {
switch (*c->json) {
case 't': return lept_parse_literal(c, v, "true", LEPT_TRUE);
case 'f': return lept_parse_literal(c, v, "false", LEPT_FALSE);
case 'n': return lept_parse_literal(c, v, "null", LEPT_NULL);
default: return lept_parse_number(c, v);//对于number的情况,可以这样处理。
case '\0': return LEPT_PARSE_EXPECT_VALUE;
}
}
/*
注:这个函数因为逻辑没有变,所以本身不用改变,需要改变的是它所调用的 lept_parse_value 函数。
*/
int lept_parse(lept_value* v, const char* json) {
/* */
}
根据JSON的数字规则写 lept_parse_number()
#include <errno.h> /* errno, ERANGE */
#include <math.h> /* HUGE_VAL */
#include <stdlib.h> /* NULL, strtod() */
#define ISDIGIT(ch) ((ch) >= '0' && (ch) <= '9')
#define ISDIGIT1TO9(ch) ((ch) >= '1' && (ch) <= '9')
/*
函数目的: 对number进行解析
思路:
1. 判断是不是负号,是:跳过
2. 整数部分:判断是否为单个0的情况,是便跳过;如果不是第一种情况,则整数部分第一个字符必须为1~9,否则返回错误码;然后有多少个digit就跳过多少。
3. 小数部分:如果出现小数点,我们跳过该小数点,然后检查它至少应有一个 digit,不是 digit 就返回错误码。跳过首个 digit,我们再检查有没有 digit,有多少个跳过多少个。
4. 指数部分:如果出现大小写 e,就表示有指数部分。跳过那个 e 之后,可以有一个正或负号,有的话就跳过。然后和小数的逻辑是一样的
*/
static int lept_parse_number(lept_context* c, lept_value* v) {
const char* p = c->json;
/* 负号 ... */
if (*p == '-') p++;
/* 整数 ... */
if (*p == '0') p++;//单个0的情况
else {
if (!ISDIGIT1TO9(*p)) return LEPT_PARSE_INVALID_VALUE;
for (p++; ISDIGIT(*p); p++);
}
/* 小数 ... */
if (*p == '.') {
p++;
if (!ISDIGIT(*p)) return LEPT_PARSE_INVALID_VALUE;
for (p++; ISDIGIT(*p); p++);
}
/* 指数 ... */
if (*p == 'e' || *p == 'E') {
p++;
if (*p == '+' || *p == '-') p++;
if (!ISDIGIT(*p)) return LEPT_PARSE_INVALID_VALUE;
for (p++; ISDIGIT(*p); p++);
}
errno = 0;
v->n = strtod(c->json, NULL);
if (errno == ERANGE && (v->n == HUGE_VAL || v->n == -HUGE_VAL))
return LEPT_PARSE_NUMBER_TOO_BIG;
v->type = LEPT_NUMBER;
c->json = p;
return LEPT_PARSE_OK;
}
上面的代码,要再说两点:
- strtod函数,将字符串转换成浮点数,这个函数的定义是 double strtod(const char str, char
**endprt) 其中 str为要转化为双精度浮点数的字符串; endptr对char的对象的引用,其值由函数设置为str中数值后的下一个字符。 - 在math.h中, 当函数的结果不可以表示为浮点数时。如果是因为结果的幅度太大以致于无法表示,则函数会设置 errno 为 ERANGE 来表示范围错误,并返回一个由宏 HUGE_VAL 或者它的否定(- HUGE_VAL)命名的一个特定的很大的值; 如果结果的幅度太小,则会返回零值。在这种情况下,error 可能会被设置为 ERANGE,也有可能不会被设置为 ERANGE。