We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JavaScript中的所有数字包括证书和小数只有一种类型:Numbr。它的实现遵循IEEE 754 标准,使用64位固定长度来表示,即标准的双精度浮点数double(单精度浮点数float则是32位)。
Numbr
IEEE 754
双精度的的精度比单精度的要高(因为存储所用的空间不同)。
IEEE754的标准,如图所示:
64位比特可分为三个部分:
符号位S:第一位是正负数符号位(sign),0表正数,1表负数
指数位E:中间的11位存储指数(exponent)
指数e可以为正可以为负,其表示小数点的位置。如 ,e就是正数4,小数点在第一个1右侧再后移4位的位置;而 0.101 用指数表示就是,e为-1。同时,IEEE754 标准要求,将此处的e+指数偏移量,得到的结果再化为二进制,就得到了我们的指数位E。如图所示: 指数偏移量公式(k为指数位个数): 如上图所示,双精度浮点数的指数位为11,即 。
指数e可以为正可以为负,其表示小数点的位置。如 ,e就是正数4,小数点在第一个1右侧再后移4位的位置;而 0.101 用指数表示就是,e为-1。同时,IEEE754 标准要求,将此处的e+指数偏移量,得到的结果再化为二进制,就得到了我们的指数位E。如图所示:
指数偏移量公式(k为指数位个数):
如上图所示,双精度浮点数的指数位为11,即 。
为什么要偏移1023?
”如果你知道为什么32位浮点数的指数偏移量是127,你就能知道为什么64位浮点数的指数偏移量是1023。 在32位浮点数中,指数位有8位,它能表示的数字是从0到2的8次方,也就是256。但是指数有正有负,所以我们需要把256这个数字从中间劈开,一半表示正数,一半表示负数,所以就是-128到+128。哦,不对,忘记了中间还有个0,所以只能表示-128到127这256个数字。那么怎么记录负数呢?一种作法是把高位置1,这样我们只要看到高位是1的就知道是负数了,所谓高位置1就是说把0到255这么多个数字劈成两半,从0到127表示正数,从128到255表示负数。但是这种作法会带来一个问题:当你比较两个数的时候,比如130和30,谁更大呢?机器会觉得130更大,但实际上130是个负数,它应该比30小才对啊。所以为了解决这个麻烦,人们发明了另外一种方法:干脆把所有数字都给它加上128得了,这样-128加上128就变成了0,而127加上128变成了255,这样的话,再比较大小,就不存在负数比正数大的情况了。 但是我要得到原来的数字怎么办呢?这好办,你只要再把指数减去128就得到了原来的数字,不是吗?比如说你读到0,那么减去128,就得到了负指数-128,读到255,减去128,就得到了127。 那为什么指数偏移是127,不是128呢?因为人们为了特殊用处,不允许使用0和255这两个数字表示指数,少了2个数字,自然就只好采用127了。 同理,64位浮点数,指数位有11位之多,2的11次方是2048,劈一半作偏移,可不就是1024吗?同理,去掉0和2048这两个数字,所以就用1023作偏移了。“
”如果你知道为什么32位浮点数的指数偏移量是127,你就能知道为什么64位浮点数的指数偏移量是1023。
在32位浮点数中,指数位有8位,它能表示的数字是从0到2的8次方,也就是256。但是指数有正有负,所以我们需要把256这个数字从中间劈开,一半表示正数,一半表示负数,所以就是-128到+128。哦,不对,忘记了中间还有个0,所以只能表示-128到127这256个数字。那么怎么记录负数呢?一种作法是把高位置1,这样我们只要看到高位是1的就知道是负数了,所谓高位置1就是说把0到255这么多个数字劈成两半,从0到127表示正数,从128到255表示负数。但是这种作法会带来一个问题:当你比较两个数的时候,比如130和30,谁更大呢?机器会觉得130更大,但实际上130是个负数,它应该比30小才对啊。所以为了解决这个麻烦,人们发明了另外一种方法:干脆把所有数字都给它加上128得了,这样-128加上128就变成了0,而127加上128变成了255,这样的话,再比较大小,就不存在负数比正数大的情况了。
但是我要得到原来的数字怎么办呢?这好办,你只要再把指数减去128就得到了原来的数字,不是吗?比如说你读到0,那么减去128,就得到了负指数-128,读到255,减去128,就得到了127。
那为什么指数偏移是127,不是128呢?因为人们为了特殊用处,不允许使用0和255这两个数字表示指数,少了2个数字,自然就只好采用127了。
同理,64位浮点数,指数位有11位之多,2的11次方是2048,劈一半作偏移,可不就是1024吗?同理,去掉0和2048这两个数字,所以就用1023作偏移了。“
尾数位M:最后的52位是尾数(mantissa),用来表示小数部分,位数不够用0补齐,超出部分进1舍0。
尾数位决定精度。因此 JavaScript 中能精准表示的最大整数就是Math.pow(2, 53),十进制即9007199254740992。
因此,计算机存储二进制构成即为:符号位+指数位+尾数位
举个栗子:
29.5转换为二进制是11101.1
11101.1转换为科学计数法:
符号位 为0(正数)
指数位 为4,加上指数偏移量1023,即1027,转为二进制即 10000000011
尾数位 为11011,补满52位即: 1101 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
所以29.5存储为计算机的二进制标准格式为
符号位+指数位+尾数位:
0+10000000011+1101 1000 0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 ,即
0100 0000 0011 1101 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
正好64位
好,现在整理一下步骤:
计算机想存储一个数字
①首先将数字转换为二进制
②再把二进制转换为科学计数法表示
③分析科学计数表示法,得出 符号位【1】+(指数位+偏移量)【11】+尾数位【52】
④拼接成64位的二进制数
表示最大的安全整数。
由上一节可知,双精度浮点数的可准确表示的最大整数是
Number.MAX_SAFE_INTEGER === Math.pow(2,53) - 1 // true
相对的,单精度浮点数因尾数位只有23位,对应的最大安全整数为 。
表示JS里内能表示的最大的值。
你或许以为就是64位全部拉满的情况:
0 11111111111 1111111111111111111111111111111111111111111111111111
但实际上,前文引用中提到过:“那为什么指数偏移是127,不是128呢?因为人们 为了特殊用处,不允许使用0和255这两个数字表示指数 ,少了2个数字,自然就只好采用127了。” 相对应的,64位存储时,11位的指数位,,即1024也会用于特殊用途。
因此,最大值的64位应该是指数位对应十进制为拉满的情况下-1,64位即:
0 11111111110 1111111111111111111111111111111111111111111111111111
计算过程是:
转换成二进制的科学计数法表示如下:
1.1111111111111111111111111111111111111111111111111111 * 2^{2046 - 1023}
= 1.1111111111111111111111111111111111111111111111111111 * 2^{1023}
= 11111111111111111111111111111111111111111111111111111 * 2^{971}
= (2^{53} - 1) * 2^{971}
= 1.7976931348623157e+308
验证一下:
(Math.pow(2, 53) - 1) * Math.pow(2, 971) // 1.7976931348623157e+308 Number.MAX_VALUE === (Math.pow(2, 53) - 1) * Math.pow(2,971) // true
到此,我们就可以很容易地理解下面精度相关的问题了。
前文提到,某些指数有特殊用途,即:
以上规则,总结如下:
我们先用前面学到的知识点来分析以下常见场景的误差产生的根本原因,最后来总结解决方案。
// 加法 ===================== 0.1 + 0.2 // 0.30000000000000004 0.7 + 0.1 // 0.7999999999999999 0.2 + 0.4 // 0.6000000000000001 // 减法 ==================== 1.5 - 1.2 // 0.30000000000000004 0.3 - 0.2 // 0.09999999999999998 // 乘法 ==================== 19.9 * 100 // 1989.9999999999998 0.8 * 3 // 2.4000000000000004 35.41 * 100 // 3540.9999999999995 // 除法 ==================== 0.3 / 0.1 // 2.9999999999999996 0.69 / 10 // 0.06899999999999999
为什么0.1+0.2 === 0.30000000000000004?
0.1转换为64位下的存储格式:
0.1
>>> 0.0001100110011001100110011001100110011001100110011001101 >>> 1.100110011001100110011001100110011001100110011001101 * 2^{-4}
>>> 0011111110111001100110011001100110011001100110011001100110011010
同理,转换0.2
0.2
>>> 0.001100110011001100110011001100110011001100110011001101
>>> 1.100110011001100110011001100110011001100110011001101 * 2^{-3}
>>> 0011111111001001100110011001100110011001100110011001100110011010
可以看出来在转换为二进制时
0.1 >>> 0.0001 1001 1001 1001...(1001无限循环) 0.2 >>> 0.0011 0011 0011 0011...(0011无限循环)
“就像一些无理数不能有限表示,如 圆周率 3.1415926...,1.3333... 等,在转换为二进制的科学记数法的形式时只保留64位有效的数字,此时只能模仿十进制进行四舍五入了,但是二进制只有 0 和 1 两个,于是变为 0 舍 1 入。在这一步出现了错误,那么一步错步步错,那么在计算机存储小数时也就理所应当的出现了误差。这即是计算机中部分浮点数运算时出现误差,这就是丢失精度的根本原因”
将0.1和0.2的二进制形式按实际展开,末尾补零相加,结果如下
0.00011001100110011001100110011001100110011001100110011010
+0.00110011001100110011001100110011001100110011001100110100
=0.01001100110011001100110011001100110011001100110011001110
0.1+0.2 >> 0.0100 1100 1100 1100...(1100无限循环)
则0.1 + 0.2的结果的二进制数科学记数法表示为为1.001100110011001100110011001100110011001100110011010 * 2^(-2), 省略尾数最后的0,即 1.00110011001100110011001100110011001100110011001101 * 2^(-2), 因此(0.1+0.2)实际存储时的形式是 0011111111010011001100110011001100110011001100110011001100110100 因计算机存储位数的限制而截断的二进制数字,再转换为十进制,就成了0.30000000000000004,刚好符合控制台里打印0.1+0.2的结果
所以,我们可以得出结论:十进制的浮点数在转换为二进制时,若出现了无限循环,会造成二进制的舍入操作,再转换为十进制时就会造成了计算误差。
9999999999999999 == 10000000000000001===true ?
大整数的精度丢失和浮点数本质上是一样的,存储二进制时小数点的偏移量最大为52位,超出就会有舍入操作,因此JavaScript中能精准表示的最大整数是Math.pow(2, 53),十进制即9007199254740992,大于9007199254740992就可能会丢失精度。
使用parseInt()时也会有这种问题。
//firefox/chrome中toFixed 兼容性问题 1.35.toFixed(1) // 1.4 正确 1.335.toFixed(2) // 1.33 错误 1.3335.toFixed(3) // 1.333 错误 1.33335.toFixed(4) // 1.3334 正确 1.333335.toFixed(5) // 1.33333 错误 1.3333335.toFixed(6) // 1.333333 错误
根本原因还是浮点数精度丢失问题:
如 1.005.toFixed(2) 返回的是 1.00 而不是 1.01
1.005.toPrecision(21) //1.00499999999999989342
ES6在Number对象上新增了一个极小的常量——Number.EPSILON
Number.EPSILON // 2.220446049250313e-16
引入这么一个小的数的值,目的在于为浮点数的计算设置一个误差范围,如果误差能够小于Number.EPSILON,我们就可以认为结果是可靠的。
测试是否相等
function equal(x, y){ return Math.abs(x - y) < Number.EPSILON } equal(0.1+0.2,0.3) // true
当你拿到可能有精度丢失的数据(如0.1+0.2),要展示时可以这样:
// Q: 为什么选12做默认精度? // A: 经验选择 function strip(num, precision = 12) { return parseFloat(num.toPrecision(precision)); } strip(0.1+0.2) // 0.3
但此方法仅用于最终结果的展示,在运算前这样处理是无意义的(计算中仍会丢失精度)。
方案1
// 将小数末位的5改成6再调用toFixed() function toFixed(number, precision) { var str = number + '' var len = str.length var last = str.substring(len - 1, len) if (last == '5') { last = '6' str = str.substring(0, len - 1) + last return (str - 0).toFixed(precision) } else { return number.toFixed(precision) } } console.log(toFixed(1.333335, 5))
方案2
// 先扩大再缩小 function toFixed(num, s) { var times = Math.pow(10, s) // 因为乘法同样存在精度问题,加上0.5保证不会扩大后尾数过多而parseInt后丢失精度 var des = num * times + 0.5 // 去除小数 des = parseInt(des, 10) / times return des + '' } console.log(toFixed(1.333335, 5))
修复常用算数运算符的方法原理都是扩大缩小法,但也有些细节要注意。
/** * floatObj 包含加减乘除四个方法,能确保浮点数运算不丢失精度 * * 精度丢失问题(或称舍入误差,其根本原因是二进制和实现位数限制有些数无法有限表示 * 以下是十进制小数对应的二进制表示 * 0.1 >> 0.0001 1001 1001 1001…(1001无限循环) * 0.2 >> 0.0011 0011 0011 0011…(0011无限循环) * 计算机里每种数据类型的存储是一个有限宽度,比如 JavaScript 使用 64 位存储数字类型,因此超出的会舍去。舍去的部分就是精度丢失的部分。 * * ** method ** * add / subtract / multiply /divide * * ** explame ** * 0.1 + 0.2 == 0.30000000000000004 (多了 0.00000000000004) * 0.2 + 0.4 == 0.6000000000000001 (多了 0.0000000000001) * 19.9 * 100 == 1989.9999999999998 (少了 0.0000000000002) * * floatObj.add(0.1, 0.2) === 0.3 * floatObj.multiply(19.9, 100) === 1990 * */ var floatObj = (function () { /* * 判断obj是否为一个整数 整数取整后还是等于自己。利用这个特性来判断是否是整数 */ function isInteger(obj) { // 或者使用 Number.isInteger() return Math.floor(obj) === obj } /* * 将一个浮点数转成整数,返回整数和倍数。如 3.14 >> 314,倍数是 100 * @param floatNum {number} 小数 * @return {object} * {times:100, num: 314} */ function toInteger(floatNum) { // 初始化数字与精度 times精度倍数 num转化后的整数 var ret = { times: 1, num: 0 } var isNegative = floatNum < 0 //是否是小数 if (isInteger(floatNum)) { // 是否是整数 ret.num = floatNum return ret //是整数直接返回 } var strfi = floatNum + '' // 转换为字符串 var dotPos = strfi.indexOf('.') var len = strfi.substr(dotPos + 1).length // 拿到小数点之后的位数 var times = Math.pow(10, len) // 精度倍数 /* 为什么加0.5? 前面讲过乘法也会出现精度问题 假设传入0.16344556此时倍数为100000000 Math.abs(0.16344556) * 100000000=0.16344556*10000000=1634455.5999999999 少了0.0000000001 加上0.5 0.16344556*10000000+0.5=1634456.0999999999 parseInt之后乘法的精度问题得以矫正 */ var intNum = parseInt(Math.abs(floatNum) * times + 0.5, 10) ret.times = times if (isNegative) { intNum = -intNum } ret.num = intNum return ret } /* * 核心方法,实现加减乘除运算,确保不丢失精度 * 思路:把小数放大为整数(乘),进行算术运算,再缩小为小数(除) * @param a {number} 运算数1 * @param b {number} 运算数2 */ function operation(a, b, op) { var o1 = toInteger(a) var o2 = toInteger(b) var n1 = o1.num // 3.25+3.153 var n2 = o2.num var t1 = o1.times var t2 = o2.times var max = t1 > t2 ? t1 : t2 var result = null switch (op) { // 加减需要根据倍数关系来处理 case 'add': if (t1 === t2) { // 两个小数倍数相同 result = n1 + n2 } else if (t1 > t2) { // o1 小数位 大于 o2 result = n1 + n2 * (t1 / t2) } else { // o1小数位小于 o2 result = n1 * (t2 / t1) + n2 } return result / max case 'subtract': if (t1 === t2) { result = n1 - n2 } else if (t1 > t2) { result = n1 - n2 * (t1 / t2) } else { result = n1 * (t2 / t1) - n2 } return result / max case 'multiply': // 325*3153/(100*1000) 扩大100倍 ==>缩小100倍 result = (n1 * n2) / (t1 * t2) return result case 'divide': // (325/3153)*(1000/100) 缩小100倍 ==>扩大100倍 result = (n1 / n2) * (t2 / t1) return result } } // 加减乘除的四个接口 function add(a, b) { return operation(a, b, 'add') } function subtract(a, b) { return operation(a, b, 'subtract') } function multiply(a, b) { return operation(a, b, 'multiply') } function divide(a, b) { return operation(a, b, 'divide') } return { add: add, subtract: subtract, multiply: multiply, divide: divide, } })() console.log(0.1 + 0.2) // 0.30000000000000004 console.log(floatObj.add(0.1, 0.2)) // 0.3 console.log(0.3 - 0.1) // 0.19999999999999998 console.log(floatObj.subtract(0.3, 0.1)) // 0.2 console.log(35.41 * 100) // 3540.9999999999995 console.log(floatObj.multiply(35.41, 100)) // 3541 console.log(0.3 / 0.1) // 2.9999999999999996 console.log(floatObj.divide()) // 3
当然,也可以用成熟的库来解决此类问题,如math.js 、number-precision 等。
math.js
number-precision
camsong/blog#9
https://zhuanlan.zhihu.com/p/100353781
https://segmentfault.com/q/1010000016401244/a-1020000016446375###
https://zh.wikipedia.org/wiki/IEEE_754#%E7%89%B9%E6%AE%8A%E5%80%BC
The text was updated successfully, but these errors were encountered:
有很多地方明显有问题了
Sorry, something went wrong.
No branches or pull requests
JavaScript的数值存储探析
学
1. 浮点数的存储规则
JavaScript中的所有数字包括证书和小数只有一种类型:
Numbr
。它的实现遵循IEEE 754
标准,使用64位固定长度来表示,即标准的双精度浮点数double(单精度浮点数float则是32位)。IEEE754的标准,如图所示:
64位比特可分为三个部分:
符号位S:第一位是正负数符号位(sign),0表正数,1表负数
指数位E:中间的11位存储指数(exponent)
为什么要偏移1023?
尾数位M:最后的52位是尾数(mantissa),用来表示小数部分,位数不够用0补齐,超出部分进1舍0。
因此,计算机存储二进制构成即为:符号位+指数位+尾数位
举个栗子:
29.5转换为二进制是11101.1
11101.1转换为科学计数法:
符号位 为0(正数)
指数位 为4,加上指数偏移量1023,即1027,转为二进制即 10000000011
尾数位 为11011,补满52位即: 1101 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
所以29.5存储为计算机的二进制标准格式为
符号位+指数位+尾数位:
0+10000000011+1101 1000 0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 ,即
0100 0000 0011 1101 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
正好64位
好,现在整理一下步骤:
计算机想存储一个数字
①首先将数字转换为二进制
②再把二进制转换为科学计数法表示
③分析科学计数表示法,得出 符号位【1】+(指数位+偏移量)【11】+尾数位【52】
④拼接成64位的二进制数
2. Number对象上的特殊值
MAX_SAFE_INTEGER
表示最大的安全整数。
由上一节可知,双精度浮点数的可准确表示的最大整数是
MAX_VALUE
表示JS里内能表示的最大的值。
你或许以为就是64位全部拉满的情况:
0 11111111111 1111111111111111111111111111111111111111111111111111
但实际上,前文引用中提到过:“那为什么指数偏移是127,不是128呢?因为人们 为了特殊用处,不允许使用0和255这两个数字表示指数 ,少了2个数字,自然就只好采用127了。” 相对应的,64位存储时,11位的指数位,,即1024也会用于特殊用途。
因此,最大值的64位应该是指数位对应十进制为拉满的情况下-1,64位即:
0 11111111110 1111111111111111111111111111111111111111111111111111
计算过程是:
0 11111111110 1111111111111111111111111111111111111111111111111111
转换成二进制的科学计数法表示如下:
1.1111111111111111111111111111111111111111111111111111 * 2^{2046 - 1023}
= 1.1111111111111111111111111111111111111111111111111111 * 2^{1023}
= 11111111111111111111111111111111111111111111111111111 * 2^{971}
= (2^{53} - 1) * 2^{971}
= 1.7976931348623157e+308
验证一下:
到此,我们就可以很容易地理解下面精度相关的问题了。
3. 特殊值的存储
前文提到,某些指数有特殊用途,即:
以上规则,总结如下:
学以致用
我们先用前面学到的知识点来分析以下常见场景的误差产生的根本原因,最后来总结解决方案。
案例分析
1.1 精度丢失
为什么0.1+0.2 === 0.30000000000000004?
0.1转换为64位下的存储格式:
0.1
>>> 0.0001100110011001100110011001100110011001100110011001101 >>> 1.100110011001100110011001100110011001100110011001101 * 2^{-4}
>>> 0011111110111001100110011001100110011001100110011001100110011010
同理,转换0.2
0.2
>>> 0.001100110011001100110011001100110011001100110011001101
>>> 1.100110011001100110011001100110011001100110011001101 * 2^{-3}
>>> 0011111111001001100110011001100110011001100110011001100110011010
可以看出来在转换为二进制时
“就像一些无理数不能有限表示,如 圆周率 3.1415926...,1.3333... 等,在转换为二进制的科学记数法的形式时只保留64位有效的数字,此时只能模仿十进制进行四舍五入了,但是二进制只有 0 和 1 两个,于是变为 0 舍 1 入。在这一步出现了错误,那么一步错步步错,那么在计算机存储小数时也就理所应当的出现了误差。这即是计算机中部分浮点数运算时出现误差,这就是丢失精度的根本原因”
将0.1和0.2的二进制形式按实际展开,末尾补零相加,结果如下
0.00011001100110011001100110011001100110011001100110011010
+0.00110011001100110011001100110011001100110011001100110100
=0.01001100110011001100110011001100110011001100110011001110
则0.1 + 0.2的结果的二进制数科学记数法表示为为1.001100110011001100110011001100110011001100110011010 * 2^(-2), 省略尾数最后的0,即 1.00110011001100110011001100110011001100110011001101 * 2^(-2), 因此(0.1+0.2)实际存储时的形式是 0011111111010011001100110011001100110011001100110011001100110100
因计算机存储位数的限制而截断的二进制数字,再转换为十进制,就成了0.30000000000000004,刚好符合控制台里打印0.1+0.2的结果
所以,我们可以得出结论:十进制的浮点数在转换为二进制时,若出现了无限循环,会造成二进制的舍入操作,再转换为十进制时就会造成了计算误差。
1.2 大数危机
9999999999999999 == 10000000000000001===true ?
大整数的精度丢失和浮点数本质上是一样的,存储二进制时小数点的偏移量最大为52位,超出就会有舍入操作,因此JavaScript中能精准表示的最大整数是Math.pow(2, 53),十进制即9007199254740992,大于9007199254740992就可能会丢失精度。
使用parseInt()时也会有这种问题。
1.3 toFixed()对于小数最后一位为5时进位不正确问题
根本原因还是浮点数精度丢失问题:
如 1.005.toFixed(2) 返回的是 1.00 而不是 1.01
2. 解决方案
“修复” 0.1+0.2 == 0.3
ES6在Number对象上新增了一个极小的常量——Number.EPSILON
引入这么一个小的数的值,目的在于为浮点数的计算设置一个误差范围,如果误差能够小于Number.EPSILON,我们就可以认为结果是可靠的。
测试是否相等
2.2 修复数据展示
当你拿到可能有精度丢失的数据(如0.1+0.2),要展示时可以这样:
但此方法仅用于最终结果的展示,在运算前这样处理是无意义的(计算中仍会丢失精度)。
修复 toFixed()
方案1
方案2
2.4 修复数据运算(+-*/)
修复常用算数运算符的方法原理都是扩大缩小法,但也有些细节要注意。
当然,也可以用成熟的库来解决此类问题,如
math.js
、number-precision
等。参考文章:
camsong/blog#9
https://zhuanlan.zhihu.com/p/100353781
https://segmentfault.com/q/1010000016401244/a-1020000016446375###
https://zh.wikipedia.org/wiki/IEEE_754#%E7%89%B9%E6%AE%8A%E5%80%BC
The text was updated successfully, but these errors were encountered: