299 lines
17 KiB
PHP
299 lines
17 KiB
PHP
<?php
|
||||
|
|
/**
|
|||
|
|
* 汉字 → 拼音首字母(用户 2026-10-03 第 4 条)。
|
|||
|
|
*
|
|||
|
|
* 用途:名册搜索支持「姓名首字母」——
|
|||
|
|
* 输入 y / yy / yyq 都能搜到「杨永全」。
|
|||
|
|
*
|
|||
|
|
* 两条路(**必须都有**,因为本地与线上环境不同):
|
|||
|
|
*
|
|||
|
|
* ① `Intl` 扩展的 `Collator('zh-Hans-CN-u-co-pinyin')`:拿一张
|
|||
|
|
* 「a..z 各挑一个锚字」的表做二分,就能判断任意汉字落在哪个字母段。
|
|||
|
|
* 线上 PHP 7.4/8.1 都有 intl,走这条最准、零数据。
|
|||
|
|
* ② 没有 intl 时的兜底:**常用姓氏/人名用字表**。
|
|||
|
|
* 本地开发环境没有 intl(实测 `intl: 无`),没有兜底就等于
|
|||
|
|
* 「这个功能本地永远测不了」—— 那是最糟的情况,只能上线才发现问题。
|
|||
|
|
*
|
|||
|
|
* ⚠️ 兜底表覆盖不到的生僻字会**原样保留首字符参与匹配**(不产字母),
|
|||
|
|
* 也就是说:搜不到时是"少匹配",绝不会错匹配。
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
declare(strict_types=1);
|
|||
|
|
|
|||
|
|
namespace App;
|
|||
|
|
|
|||
|
|
final class Pinyin
|
|||
|
|
{
|
|||
|
|
/** 拼音排序用的锚字(a..z 各一个;没有 i/u/v 开头的常用字,所以是 23 个) */
|
|||
|
|
private const ANCHORS = '阿八擦搭娥发嘎哈击喀垃妈拿哦啪期然撒塌挖昔压匝';
|
|||
|
|
private const LETTERS = 'abcdefghjklmnopqrstwxyz';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 常用姓氏 + 常见人名字的**首字母表**(兜底用)。
|
|||
|
|
* 只列"作为名字第一个字"出现频率高的;查不到就交给下一档处理。
|
|||
|
|
* 这一份不可能穷尽,所以它只是兜底 —— 线上有 intl 时走的是锚字二分。
|
|||
|
|
*/
|
|||
|
|
private const TABLE = [
|
|||
|
|
// 姓氏(覆盖绝大多数中国姓氏)
|
|||
|
|
'赵a' => 1, '钱q' => 1, '孙s' => 1, '李l' => 1, '周z' => 1, '吴w' => 1, '郑z' => 1,
|
|||
|
|
'王w' => 1, '冯f' => 1, '陈c' => 1, '褚c' => 1, '卫w' => 1, '蒋j' => 1, '沈s' => 1,
|
|||
|
|
'韩h' => 1, '杨y' => 1, '朱z' => 1, '秦q' => 1, '尤y' => 1, '许x' => 1, '何h' => 1,
|
|||
|
|
'吕l' => 1, '施s' => 1, '张z' => 1, '孔k' => 1, '曹c' => 1, '严y' => 1, '华h' => 1,
|
|||
|
|
'金j' => 1, '魏w' => 1, '陶t' => 1, '姜j' => 1, '戚q' => 1, '谢x' => 1, '邹z' => 1,
|
|||
|
|
'喻y' => 1, '柏b' => 1, '水s' => 1, '窦d' => 1, '章z' => 1, '云y' => 1, '苏s' => 1,
|
|||
|
|
'潘p' => 1, '葛g' => 1, '奚x' => 1, '范f' => 1, '彭p' => 1, '郎l' => 1, '鲁l' => 1,
|
|||
|
|
'韦w' => 1, '昌c' => 1, '马m' => 1, '苗m' => 1, '凤f' => 1, '花h' => 1, '方f' => 1,
|
|||
|
|
'俞y' => 1, '任r' => 1, '袁y' => 1, '柳l' => 1, '酆f' => 1, '鲍b' => 1, '史s' => 1,
|
|||
|
|
'唐t' => 1, '费f' => 1, '廉l' => 1, '岑c' => 1, '薛x' => 1, '雷l' => 1, '贺h' => 1,
|
|||
|
|
'倪n' => 1, '汤t' => 1, '滕t' => 1, '殷y' => 1, '罗l' => 1, '毕b' => 1, '郝h' => 1,
|
|||
|
|
'邬w' => 1, '安a' => 1, '常c' => 1, '乐y' => 1, '于y' => 1, '时s' => 1, '傅f' => 1,
|
|||
|
|
'皮p' => 1, '卞b' => 1, '齐q' => 1, '康k' => 1, '伍w' => 1, '余y' => 1, '元y' => 1,
|
|||
|
|
'卜b' => 1, '顾g' => 1, '孟m' => 1, '平p' => 1, '黄h' => 1, '和h' => 1, '穆m' => 1,
|
|||
|
|
'萧x' => 1, '尹y' => 1, '姚y' => 1, '邵s' => 1, '湛z' => 1, '汪w' => 1, '祁q' => 1,
|
|||
|
|
'毛m' => 1, '禹y' => 1, '狄d' => 1, '米m' => 1, '贝b' => 1, '明m' => 1, '臧z' => 1,
|
|||
|
|
'计j' => 1, '伏f' => 1, '成c' => 1, '戴d' => 1, '谈t' => 1, '宋s' => 1, '茅m' => 1,
|
|||
|
|
'庞p' => 1, '熊x' => 1, '纪j' => 1, '舒s' => 1, '屈q' => 1, '项x' => 1, '祝z' => 1,
|
|||
|
|
'董d' => 1, '梁l' => 1, '杜d' => 1, '阮r' => 1, '蓝l' => 1, '闵m' => 1, '席x' => 1,
|
|||
|
|
'季j' => 1, '麻m' => 1, '强q' => 1, '贾j' => 1, '路l' => 1, '娄l' => 1, '危w' => 1,
|
|||
|
|
'江j' => 1, '童t' => 1, '颜y' => 1, '郭g' => 1, '梅m' => 1, '盛s' => 1, '林l' => 1,
|
|||
|
|
'刁d' => 1, '钟z' => 1, '徐x' => 1, '邱q' => 1, '骆l' => 1, '高g' => 1, '夏x' => 1,
|
|||
|
|
'蔡c' => 1, '田t' => 1, '樊f' => 1, '胡h' => 1, '凌l' => 1, '霍h' => 1, '虞y' => 1,
|
|||
|
|
'万w' => 1, '支z' => 1, '柯k' => 1, '昝z' => 1, '管g' => 1, '卢l' => 1, '莫m' => 1,
|
|||
|
|
'房f' => 1, '裘q' => 1, '缪m' => 1, '干g' => 1, '解x' => 1, '应y' => 1, '宗z' => 1,
|
|||
|
|
'丁d' => 1, '宣x' => 1, '邓d' => 1, '单s' => 1, '杭h' => 1, '洪h' => 1, '包b' => 1,
|
|||
|
|
'诸z' => 1, '左z' => 1, '石s' => 1, '崔c' => 1, '吉j' => 1, '钮n' => 1, '龚g' => 1,
|
|||
|
|
'程c' => 1, '嵇j' => 1, '邢x' => 1, '滑h' => 1, '裴p' => 1, '陆l' => 1, '荣r' => 1,
|
|||
|
|
'翁w' => 1, '荀x' => 1, '羊y' => 1, '於y' => 1, '惠h' => 1, '甄z' => 1, '曲q' => 1,
|
|||
|
|
'封f' => 1, '芮r' => 1, '羿y' => 1, '储c' => 1, '靳j' => 1, '汲j' => 1, '邴b' => 1,
|
|||
|
|
'糜m' => 1, '松s' => 1, '井j' => 1, '段d' => 1, '富f' => 1, '巫w' => 1, '乌w' => 1,
|
|||
|
|
'焦j' => 1, '巴b' => 1, '弓g' => 1, '牧m' => 1, '隗k' => 1, '山s' => 1, '谷g' => 1,
|
|||
|
|
'车c' => 1, '侯h' => 1, '宓m' => 1, '蓬p' => 1, '全q' => 1, '郗x' => 1, '班b' => 1,
|
|||
|
|
'仰y' => 1, '秋q' => 1, '仲z' => 1, '伊y' => 1, '宫g' => 1, '宁n' => 1, '仇q' => 1,
|
|||
|
|
'栾l' => 1, '暴b' => 1, '甘g' => 1, '厉l' => 1, '戎r' => 1, '祖z' => 1, '武w' => 1,
|
|||
|
|
'符f' => 1, '刘l' => 1, '景j' => 1, '詹z' => 1, '束s' => 1, '龙l' => 1, '叶y' => 1,
|
|||
|
|
'幸x' => 1, '司s' => 1, '韶s' => 1, '郜g' => 1, '黎l' => 1, '蓟j' => 1, '薄b' => 1,
|
|||
|
|
'印y' => 1, '宿s' => 1, '白b' => 1, '怀h' => 1, '蒲p' => 1, '邰t' => 1, '从c' => 1,
|
|||
|
|
'鄂e' => 1, '索s' => 1, '咸x' => 1, '籍j' => 1, '赖l' => 1, '卓z' => 1, '蔺l' => 1,
|
|||
|
|
'屠t' => 1, '蒙m' => 1, '池c' => 1, '乔q' => 1, '阴y' => 1, '郁y' => 1, '胥x' => 1,
|
|||
|
|
'能n' => 1, '苍c' => 1, '双s' => 1, '闻w' => 1, '莘s' => 1, '党d' => 1, '翟z' => 1,
|
|||
|
|
'谭t' => 1, '贡g' => 1, '劳l' => 1, '逄p' => 1, '姬j' => 1, '申s' => 1, '扶f' => 1,
|
|||
|
|
'堵d' => 1, '冉r' => 1, '宰z' => 1, '郦l' => 1, '雍y' => 1, '郤x' => 1, '璩q' => 1,
|
|||
|
|
'桑s' => 1, '桂g' => 1, '濮p' => 1, '牛n' => 1, '寿s' => 1, '通t' => 1, '边b' => 1,
|
|||
|
|
'扈h' => 1, '燕y' => 1, '冀j' => 1, '郏j' => 1, '浦p' => 1, '尚s' => 1, '农n' => 1,
|
|||
|
|
'温w' => 1, '别b' => 1, '庄z' => 1, '晏y' => 1, '柴c' => 1, '瞿q' => 1, '阎y' => 1,
|
|||
|
|
'充c' => 1, '慕m' => 1, '连l' => 1, '茹r' => 1, '习x' => 1, '宦h' => 1, '艾a' => 1,
|
|||
|
|
'鱼y' => 1, '容r' => 1, '向x' => 1, '古g' => 1, '易y' => 1, '慎s' => 1, '戈g' => 1,
|
|||
|
|
'廖l' => 1, '庾y' => 1, '终z' => 1, '暨j' => 1, '居j' => 1, '衡h' => 1, '步b' => 1,
|
|||
|
|
'都d' => 1, '耿g' => 1, '满m' => 1, '弘h' => 1, '匡k' => 1, '国g' => 1, '文w' => 1,
|
|||
|
|
'寇k' => 1, '广g' => 1, '禄l' => 1, '阙q' => 1, '东d' => 1, '欧o' => 1, '殳s' => 1,
|
|||
|
|
'沃w' => 1, '利l' => 1, '蔚w' => 1, '越y' => 1, '夔k' => 1, '隆l' => 1, '师s' => 1,
|
|||
|
|
'巩g' => 1, '厍s' => 1, '聂n' => 1, '晁c' => 1, '勾g' => 1, '敖a' => 1, '融r' => 1,
|
|||
|
|
'冷l' => 1, '訾z' => 1, '辛x' => 1, '阚k' => 1, '那n' => 1, '简j' => 1, '饶r' => 1,
|
|||
|
|
'空k' => 1, '曾z' => 1, '毋w' => 1, '沙s' => 1, '乜n' => 1, '养y' => 1, '鞠j' => 1,
|
|||
|
|
'须x' => 1, '丰f' => 1, '巢c' => 1, '关g' => 1, '蒯k' => 1, '相x' => 1, '查z' => 1,
|
|||
|
|
'后h' => 1, '荆j' => 1, '红h' => 1, '游y' => 1, '竺z' => 1, '权q' => 1, '逯l' => 1,
|
|||
|
|
'盖g' => 1, '益y' => 1, '桓h' => 1, '公g' => 1,
|
|||
|
|
// 名字里高频的字
|
|||
|
|
'永y' => 1, '全q' => 1, '博b' => 1, '为w' => 1, '明m' => 1, '华h' => 1, '建j' => 1,
|
|||
|
|
'国g' => 1, '文w' => 1, '军j' => 1, '平p' => 1, '志z' => 1, '强q' => 1, '磊l' => 1,
|
|||
|
|
'洋y' => 1, '勇y' => 1, '艳y' => 1, '杰j' => 1, '娟j' => 1, '涛t' => 1, '超c' => 1,
|
|||
|
|
'敏m' => 1, '静j' => 1, '丽l' => 1, '秀x' => 1, '英y' => 1, '娜n' => 1, '丹d' => 1,
|
|||
|
|
'玲l' => 1, '芳f' => 1, '燕y' => 1, '红h' => 1, '霞x' => 1, '梅m' => 1, '兰l' => 1,
|
|||
|
|
'琴q' => 1, '云y' => 1, '飞f' => 1, '鹏p' => 1, '宇y' => 1, '浩h' => 1, '鑫x' => 1,
|
|||
|
|
'亮l' => 1, '刚g' => 1, '辉h' => 1, '峰f' => 1, '龙l' => 1, '波b' => 1, '斌b' => 1,
|
|||
|
|
'春c' => 1, '雨y' => 1, '晨c' => 1, '阳y' => 1, '雪x' => 1, '悦y' => 1, '欣x' => 1,
|
|||
|
|
'怡y' => 1, '佳j' => 1, '思s' => 1, '雨y' => 1, '子z' => 1, '小x' => 1, '大d' => 1,
|
|||
|
|
'老l' => 1, '爸b' => 1, '妈m' => 1, '哥g' => 1, '姐j' => 1, '弟d' => 1, '妹m' => 1,
|
|||
|
|
'爷y' => 1, '奶n' => 1, '叔s' => 1, '姨y' => 1, '舅j' => 1, '嫂s' => 1, '儿e' => 1,
|
|||
|
|
'女n' => 1, '宝b' => 1, '乐l' => 1, '安a' => 1, '康k' => 1, '福f' => 1, '寿s' => 1,
|
|||
|
|
'喜x' => 1, '顺s' => 1, '和h' => 1, '美m' => 1, '好h' => 1, '新x' => 1, '中z' => 1,
|
|||
|
|
'正z' => 1, '长c' => 1, '海h' => 1, '江j' => 1, '河h' => 1, '山s' => 1, '林l' => 1,
|
|||
|
|
'木m' => 1, '金j' => 1, '玉y' => 1, '石s' => 1, '田t' => 1, '力l' => 1, '学x' => 1,
|
|||
|
|
'生s' => 1, '成c' => 1, '立l' => 1, '德d' => 1, '仁r' => 1, '义y' => 1, '礼l' => 1,
|
|||
|
|
'智z' => 1, '信x' => 1, '忠z' => 1, '孝x' => 1, '友y' => 1, '爱a' => 1, '慈c' => 1,
|
|||
|
|
// 家谱称呼里高频的那批字(父/母/伯/叔/姑/舅/姨/岳…)——
|
|||
|
|
// 名册搜索经常会拿这些字当关键词,兜底表里缺了就会出现
|
|||
|
|
// "本地搜 s 搜不到『张三』"这种与环境相关的怪现象。
|
|||
|
|
'父f' => 1, '母m' => 1, '祖z' => 1, '孙s' => 1, '外w' => 1, '表b' => 1, '堂t' => 1,
|
|||
|
|
'侄z' => 1, '甥s' => 1, '婿x' => 1, '媳x' => 1, '伯b' => 1, '姑g' => 1, '岳y' => 1,
|
|||
|
|
'公g' => 1, '婆p' => 1, '妻q' => 1, '夫f' => 1, '丈z' => 1, '娘n' => 1, '爹d' => 1,
|
|||
|
|
'三s' => 1, '四s' => 1, '五w' => 1, '六l' => 1, '七q' => 1, '八b' => 1, '九j' => 1,
|
|||
|
|
'十s' => 1, '百b' => 1, '千q' => 1, '万w' => 1, '一y' => 1, '二e' => 1, '零l' => 1,
|
|||
|
|
'不b' => 1, '的d' => 1, '了l' => 1, '在z' => 1, '有y' => 1, '是s' => 1, '我w' => 1,
|
|||
|
|
'人r' => 1, '家j' => 1, '门m' => 1, '户h' => 1, '先x' => 1, '后h' => 1, '晚w' => 1,
|
|||
|
|
'长c' => 1, '幼y' => 1, '大d' => 1, '小x' => 1, '多d' => 1, '少s' => 1, '高g' => 1,
|
|||
|
|
'矮a' => 1, '胖p' => 1, '瘦s' => 1, '黑h' => 1, '白b' => 1, '花h' => 1, '草c' => 1,
|
|||
|
|
'树s' => 1, '叶y' => 1, '果g' => 1, '苗m' => 1, '阳y' => 1, '光g' => 1, '月y' => 1,
|
|||
|
|
'星x' => 1, '风f' => 1, '雪x' => 1, '雨y' => 1, '雷l' => 1, '电d' => 1, '冰b' => 1,
|
|||
|
|
'州z' => 1, '城c' => 1, '村c' => 1, '镇z' => 1, '县x' => 1, '市s' => 1, '省s' => 1,
|
|||
|
|
'华h' => 1, '青q' => 1, '岛d' => 1, '京j' => 1, '津j' => 1, '冀j' => 1, '鲁l' => 1,
|
|||
|
|
'思s' => 1, '念n' => 1, '想x' => 1, '记j' => 1, '忘w' => 1, '知z' => 1, '道d' => 1,
|
|||
|
|
'说s' => 1, '话h' => 1, '听t' => 1, '看k' => 1, '走z' => 1, '来l' => 1, '去q' => 1,
|
|||
|
|
'回h' => 1, '到d' => 1, '给g' => 1, '拿n' => 1, '做z' => 1, '用y' => 1, '买m' => 1,
|
|||
|
|
'卖m' => 1, '吃c' => 1, '喝h' => 1, '睡s' => 1, '起q' => 1, '坐z' => 1, '站z' => 1,
|
|||
|
|
'跑p' => 1, '跳t' => 1, '唱c' => 1, '写x' => 1, '读d' => 1, '画h' => 1, '算s' => 1,
|
|||
|
|
'作z' => 1, '课k' => 1, '班b' => 1, '校x' => 1, '师s' => 1, '医y' => 1, '院y' => 1,
|
|||
|
|
'药y' => 1, '病b' => 1, '健j' => 1, '体t' => 1, '身s' => 1, '心x' => 1, '手s' => 1,
|
|||
|
|
'脚j' => 1, '眼y' => 1, '耳e' => 1, '鼻b' => 1, '口k' => 1, '头t' => 1, '发f' => 1,
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
/** 首字母查表:'杨' => 'y'。首次调用时从 TABLE 摊平,之后走内存缓存。 */
|
|||
|
|
private static ?array $map = null;
|
|||
|
|
|
|||
|
|
public static function initialMap(): array
|
|||
|
|
{
|
|||
|
|
if (self::$map === null) {
|
|||
|
|
self::$map = [];
|
|||
|
|
foreach (self::TABLE as $kv => $_) {
|
|||
|
|
$k = (string)$kv;
|
|||
|
|
if (mb_strlen($k, 'UTF-8') >= 1) {
|
|||
|
|
$ch = mb_substr($k, 0, 1, 'UTF-8');
|
|||
|
|
$ch = mb_substr($k, 0, mb_strlen($k, 'UTF-8') - 1, 'UTF-8');
|
|||
|
|
$letter = mb_substr((string)$kv, mb_strlen((string)$kv, 'UTF-8') - 1, 1, 'UTF-8');
|
|||
|
|
if ($ch !== '' && $letter !== '') {
|
|||
|
|
self::$map[$ch] = strtolower($letter);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return self::$map;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 一个字的拼音首字母(小写)。认不出时返回 ''。
|
|||
|
|
* 有 intl 走锚字二分;没有就走内置表。
|
|||
|
|
*/
|
|||
|
|
public static function initial(string $ch): string
|
|||
|
|
{
|
|||
|
|
if ($ch === '') {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
// 已经是 ASCII 字母 → 直接用
|
|||
|
|
if (preg_match('/^[a-zA-Z]$/', $ch)) {
|
|||
|
|
return strtolower($ch);
|
|||
|
|
}
|
|||
|
|
if (preg_match('/^[0-9]$/', $ch)) {
|
|||
|
|
return $ch;
|
|||
|
|
}
|
|||
|
|
// ① intl 拼音排序(线上有)
|
|||
|
|
$viaIntl = self::initialViaIntl($ch);
|
|||
|
|
if ($viaIntl !== null) {
|
|||
|
|
return $viaIntl;
|
|||
|
|
}
|
|||
|
|
// ② 内置表兜底(本地无 intl 时)
|
|||
|
|
return self::initialMap()[$ch] ?? '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 用 Intl 的拼音 collation + 锚字二分定位;没有 intl 返回 null */
|
|||
|
|
private static function initialViaIntl(string $ch): ?string
|
|||
|
|
{
|
|||
|
|
static $coll = false;
|
|||
|
|
if ($coll === false) {
|
|||
|
|
$coll = null;
|
|||
|
|
if (class_exists('\\Collator')) {
|
|||
|
|
$c = new \Collator('zh-Hans-CN-u-co-pinyin');
|
|||
|
|
// 排序没真的生效时(compare('阿','匝') 不 < 0)就别用,免得给出错结果
|
|||
|
|
if ($c->compare('阿', '匝') < 0) {
|
|||
|
|
$coll = $c;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if ($coll === null) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
$anchors = self::ANCHORS;
|
|||
|
|
$len = mb_strlen($anchors, 'UTF-8');
|
|||
|
|
$lo = 0;
|
|||
|
|
$hi = $len - 1;
|
|||
|
|
$best = -1;
|
|||
|
|
while ($lo <= $hi) {
|
|||
|
|
$mid = intdiv($lo + $hi, 2);
|
|||
|
|
$a = mb_substr($anchors, $mid, 1, 'UTF-8');
|
|||
|
|
if ($coll->compare($ch, $a) >= 0) {
|
|||
|
|
$best = $mid;
|
|||
|
|
$lo = $mid + 1;
|
|||
|
|
} else {
|
|||
|
|
$hi = $mid - 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if ($best < 0) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
return mb_substr(self::LETTERS, $best, 1, 'UTF-8');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 一串字 → 首字母串(小写)。认不出的字**跳过**(不产字母,也不报错)。
|
|||
|
|
* 例:'杨永全' → 'yyq'。
|
|||
|
|
*/
|
|||
|
|
public static function initials(string $s): string
|
|||
|
|
{
|
|||
|
|
$out = '';
|
|||
|
|
$n = mb_strlen($s, 'UTF-8');
|
|||
|
|
for ($i = 0; $i < $n; $i++) {
|
|||
|
|
$out .= self::initial(mb_substr($s, $i, 1, 'UTF-8'));
|
|||
|
|
}
|
|||
|
|
return $out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* q 是否是 s 的**子序列**(用于"输入 y 也能搜到 yyq"这类前缀式检索)。
|
|||
|
|
*
|
|||
|
|
* 为什么用子序列而不是前缀:
|
|||
|
|
* 用户可能只记得中间某个字(比如"永全" → yq),或干脆跳着打;
|
|||
|
|
* 名册这种小列表宁可宽松一点,也别让人以为搜索坏了。
|
|||
|
|
*/
|
|||
|
|
public static function isSubsequence(string $q, string $s): bool
|
|||
|
|
{
|
|||
|
|
$q = strtolower(trim($q));
|
|||
|
|
$s = strtolower($s);
|
|||
|
|
if ($q === '') {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
$i = 0;
|
|||
|
|
$qn = strlen($q);
|
|||
|
|
for ($j = 0, $sn = strlen($s); $j < $sn && $i < $qn; $j++) {
|
|||
|
|
if ($s[$j] === $q[$i]) {
|
|||
|
|
$i++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return $i === $qn;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 这个人是否匹配搜索词。
|
|||
|
|
*
|
|||
|
|
* 覆盖:姓名 / 称呼 / 邮箱 / 手机号 的**模糊**包含,以及姓名首字母的**子序列**匹配。
|
|||
|
|
*
|
|||
|
|
* @param array $row 名册行(含 name / email / phone_plain,可选 my_nickname)
|
|||
|
|
* @param string $q 搜索词
|
|||
|
|
*/
|
|||
|
|
public static function matches(array $row, string $q): bool
|
|||
|
|
{
|
|||
|
|
$q = trim($q);
|
|||
|
|
if ($q === '') {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
$needle = mb_strtolower($q, 'UTF-8');
|
|||
|
|
foreach (['name', 'my_nickname', 'nickname', 'email', 'phone_plain'] as $k) {
|
|||
|
|
$v = mb_strtolower(trim((string)($row[$k] ?? '')), 'UTF-8');
|
|||
|
|
if ($v !== '' && mb_strpos($v, $needle) !== false) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 首字母:只对**字母/数字**查询词做子序列匹配,
|
|||
|
|
// 中文查询词走上面的"包含"(否则"永全"会被当成首字母串去比对,永远匹配不上)
|
|||
|
|
if (preg_match('/^[a-z0-9]+$/', $needle)) {
|
|||
|
|
$ini = self::initials((string)($row['name'] ?? ''));
|
|||
|
|
if ($ini !== '' && self::isSubsequence($needle, $ini)) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
// 称呼也参与首字母(比如称呼是"小宝" → xb)
|
|||
|
|
$nickIni = self::initials((string)($row['my_nickname'] ?? ''));
|
|||
|
|
if ($nickIni !== '' && self::isSubsequence($needle, $nickIni)) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|