继:我朝特有需求之《英文字符占 0.5 个,中文字符占 1 个》

之所以写继篇,其实我是来自我检讨的,上一次发表了 《ThinkSNS+ 是如何计算字符显示长度的(使用 Laravel 自定义验证规则)》 后,下面有一个网友多方测试告诉我说 str_word_count 是有问题的,但是我的环境下确实成功的,所以一直在想一个兼容性更高的方法。

这短时间一直在开发 ThinkSNS+ 的支付功能,所以一直没时间看,今天上午可能没有什么感觉写代码,干脆就来像一个兼容性更好的计算方法。

需求

重新说下需求,在我天朝 PM 经常会提一种要求,就是例如 一个用户名做多输入 12 个汉字,但是英文可以输入 24 个,混排也要满足这个规则,也就是 单字节字符占 0.5 多子节字符占 1这样的计算。

解决

闲下来的时候看了上篇文字发的计算法法,其实是没什么问题的,就是兼容性不好,而且写那个方法的时候也是想多了,根本没有那么复杂,我们看上一篇中计算多子节的方法:

$double = str_word_count(preg_replace('([a-zA-Z0-9_])', '', $value));

计算方法是剔除我们需求中允许的单字节,然后通过 str_word_count 来获取多子节的个数,其实这个函数是可以获取到的,但是部分系统下是不会成功的,其实还有一个函数就可以直接获取多子节的个数 mb_strlen 修改后如下:

第一想法是,当时傻逼了。

$double = mb_strlen(preg_replace('([a-zA-Z0-9_])', '', $str));

然后我们就可以正确完成这个需求了,最终实现的 Laravel 验证规则如下:

// 添加长度规则
Validator::extend('display_length', function ($attribute, $value, array $parameters) {
    if (empty($parameters)) {
        throw new \InvalidArgumentException('Parameters must be passed');
    }

    $min = 0;
    if (count($parameters) === 1) {
        list($max) = $parameters;
    } elseif (count($parameters) >= 2) {
        list($min, $max) = $parameters;
    }

    if (! isset($max) || $max < $min) {
        throw new \InvalidArgumentException('The parameters passed are incorrect');
    }

    // 计算单字节.
    preg_match_all('/[a-zA-Z0-9_]/', $value, $single);
    $single = count($single[0]) / 2;

    // 多子节长度.
    $double = mb_strlen(preg_replace('([a-zA-Z0-9_])', '', $value));

    $length = $single + $double;

    return $length >= $min && $length <= $max;
});

laravel 通过上面的规则,可以在表单验证规则中直接验证了,还支持传入最小值和最大值。


上面的代码都是来自于 基于 Laravel 开发的开源程序 ThinkSNS+ 中,ThinkSNS+ 采用 apache-2.0 协议开源,我相信可以作为很多 Laravel 学习者的学习程序之一。

喜欢的话帮忙点个 star ,开源不易,感谢大家的支持。

GitHub: https://github.com/zhiyicx/thinksns-plus

本作品采用《CC 协议》,转载必须注明作者和本文链接
Seven 的代码太渣,欢迎关注我的新拓展包 medz/cors 解决 PHP 项目程序设置跨域需求。
本帖由系统于 5年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 17
medz

大家还有上面好的方法吗?一起交流交流。

6年前 评论

感谢分享,我的需求是文章标题中有中英文的时候,截取固定长度导致长短不一,下午用你的方法实现下看看效果。

6年前 评论
medz

@yaobaliu windows 平台下,可能还是会有长短不一的问题,但是很少,基本是出现在有 emoji 的需求下,因为 emoji 在windows 平台并非和中文等长,emoji 是 1.3 个中文长度。

6年前 评论
TimJuly

最简单的方法往往是你最看不上的方法,php 内置有计算字符长度的方法strlen

So, this is the code:

function the_fucking_length_function($str)
{
    return strlen(mb_convert_encoding($str, 'GB2312')) / 2;
}

PS:袁记串串挺好吃的?

6年前 评论
medz

@TimJuly 这不是最简单的方法,你的就是转为 gbk,然后计算,在几年前的确是这样搞的,这种方法现在已经不适用了其实。尤其是你可能忽略中中文只有两万多转为 gbk 这个方法是正确的,这个方法阶等:

$length = (strlen($value) + mb_strlen($value)) / 2;

如果是中文外呢?emoji呢?我上面之所以这样考虑就是考虑到 除了单字节外所有多子节语言,例如 emoji,韩语,日语,阿拉伯语等。

6年前 评论
TimJuly

@medz 你不是说"英文字符占 0.5 个,中文字符占 1 个"么.

你这是没有需求给自己创造需求啊,为啥统计中文比英文多一倍呢,因为绝大多数字体中文的宽度是英文的两倍,这样展示出来宽度一致,你还想把全球的语言都考虑进去,那你考虑非等宽字体怎么按长度展示了么?

6年前 评论
 function mbstrlen($str) {
    return ceil((strlen($str) + mb_strlen($str, "UTF8")) / 2);
}

一直这么用。。

6年前 评论
medz

@TimJuly 除错了~是 ➗4 。。。发出去前没看代码,就顺手打了 2。

6年前 评论
medz

@terranc 其实吧,这个方法是好的,缺点就只有一个,就是精度问题,因为utf-8 中只有两万多个汉字是 3 字节,还有六万多的不常用汉字是 4 字节的,附带 emoji之类的就。。。你懂的~

6年前 评论
medz

@TimJuly 上一篇文章中说了,国内 pm 有很多这种需求,app 设计也是设计的 2 英文字母占的是1汉字等~ 并不是没有需求制造需求 2 英文 = 1 汉字需求国内肯定不少,其次你说的没有需求制造需求肯能是说我要兼容其他语言,例如韩语等,这个不难理解吧,现在手机上随时可以输入 emoji 了,其他语言实现的文字表情用户不发?我在很多平台看到下面都有用户发这些。

6年前 评论
medz

我整理下计算代码:

function str_display_len (string $str)
{
    preg_match_all('/[a-zA-Z0-9_]/', $str, $single);

    return count($single[0]) / 2 + mb_strlen(preg_replace('([a-zA-Z0-9_])', '', $str));
}
6年前 评论

@medz 要说到 emoji 就确实问题多了,但我觉得一般需要截断的地方都先过滤掉 emoji,就像需要截断的地方一般都过滤掉 html tag 一样。

6年前 评论
medz

@terranc 其实我总结了下,计算方法目前应该是有两个主流,和我分享的这个:

  • 转成 GBK / 2
  • 过滤特殊字节,然后三子节 (strlen + mb_strlen) / 4
  • 过渡单字节,mb_strlen + single / 2

    其实都能实现,转 gbk 代码层面看起来少一些,过滤特殊字节就是 @terranc 的方法,可能更冗余点(这个是网上的常规方法),过滤单字节呢,可能看起来挺复杂。

6年前 评论
medz

其实 @TimJuly 说的挺对的,最简单的方法往往是看不上的方法。我们不考虑国外的情况下,直接转成 gbk 之后除 2 ,能最简单最快的得到结果。?

6年前 评论

gbk 还是不够吧,虽然不考虑多国,香港也是中国的呀,gbk里的繁体还是不全的。

6年前 评论
medz

@terranc 其实我之所以分享剔除单字节的方法,也是因为考虑其他语言,因为我们的用户名的验证规则采用正则 /^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/ 而有了这个正则,用户可以输入任何非数字和规定非限定特殊字符开头的用户名,因为是使用 Laravel 表单验证的原因,我们有了允许字符规则后,还需要有一个长度规则,pm的要求就是 2 英文或者符号 = 1 其他字符。所以有了剔除单字节的方法。


不过我测试了下转 gbk ,例如 emoji,一些繁体字,韩语,阿拉伯语,转换为 2 字节的非法字符。其实emoji 等是可以争取算出长度的,但是个人觉得系统转码性能消耗远比剔除单字节的正则消耗大。

6年前 评论

mb_strwidth
多字节字符通常是单字节字符的两倍宽度。

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
创始人 @ Odore Inc.
文章
33
粉丝
202
喜欢
532
收藏
198
排名:23
访问:24.7 万
私信
所有博文
社区赞助商