Files
tougao/application/common/AuthorListFormatter.php
2026-08-07 17:51:18 +08:00

202 lines
6.3 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common;
/**
* 作者列表著录规则:作者数不超过 MAX_LISTED 时全部列出,超过时只列前 KEEP 位再加 et al。
*
* 单独抽出来是因为"数作者个数"并不能简单按逗号切:
* - 温哥华式 "Smith AB, Jones AC" 逗号 = 作者分隔
* - APA 式 "Smith, A. B., Jones, A. C." 逗号既分隔作者,也分隔姓与名
* - 机构作者 "Department of Health, Education, and Welfare" 逗号只是机构名的一部分
* 按逗号数会把 APA 的 3 位作者数成 6 位,凭空触发截断。
*
* 拿不准的一律少切(宁可多列几位,也不要把不该截的截掉)。
*/
class AuthorListFormatter
{
/** 全部列出的上限 */
const MAX_LISTED = 6;
/** 超过上限时保留的作者数 */
const KEEP = 3;
/**
* 按著录规则输出作者串
*/
public static function format($author, $max = self::MAX_LISTED, $keep = self::KEEP)
{
$author = self::normalize($author);
if ($author === '') {
return '';
}
// 已经是 "…, et al" 形态的(历史数据或上游截断过),保持原样,不再叠加
if (self::isTruncated($author)) {
return $author;
}
$units = self::split($author);
if (empty($units)) {
return $author;
}
$max = max(1, (int)$max);
$keep = min(max(1, (int)$keep), $max);
if (count($units) > $max) {
return implode(', ', array_slice($units, 0, $keep)) . ', et al';
}
return implode(', ', $units);
}
public static function countAuthors($author)
{
return count(self::split($author));
}
public static function isTruncated($author)
{
return (bool)preg_match('/\bet\s+al\.?\s*$/iu', (string)$author);
}
/**
* 拆成单个作者
*
* @return string[]
*/
public static function split($author)
{
$author = self::normalize($author);
$author = preg_replace('/[,;]?\s*\bet\s+al\.?\s*$/iu', '', $author);
$author = trim($author, " ,;.");
if ($author === '') {
return [];
}
if (strpos($author, ';') !== false) {
// 分号是无歧义的作者分隔符
$units = preg_split('/\s*;\s*/u', $author);
} else {
$text = $author;
// "A, B, and C" 里的 and/& 才是分隔符;没有逗号时它多半是机构名的一部分
// (如 "National Institute for Health and Care Excellence"),不能切
if (strpos($text, ',') !== false) {
$text = preg_replace('/\s*,?\s+(?:and|&)\s+/iu', ', ', $text);
}
$units = preg_split('/\s*,\s*/u', $text);
}
$units = array_values(array_filter(array_map(function ($u) {
return trim($u, " ,;");
}, (array)$units), 'strlen'));
$units = self::mergeGivenNames($units);
// 整串没有一个单元像人名,多半是带逗号的机构名,算一位作者
$hasPerson = false;
foreach ($units as $unit) {
if (self::looksLikePerson($unit)) {
$hasPerson = true;
break;
}
}
return $hasPerson ? $units : [$author];
}
/**
* 合并 APA 的 "姓, 名" —— "Smith", "A. B." → "Smith, A. B."
*
* @param string[] $units
* @return string[]
*/
private static function mergeGivenNames(array $units)
{
$merged = [];
foreach ($units as $unit) {
// 上一位作者已经带了名缩写,说明这一段是新作者的姓,不能再往上并
$last = empty($merged) ? '' : $merged[count($merged) - 1];
if ($last !== '' && !self::hasInitials($last) && self::looksLikeGivenName($unit)) {
$merged[count($merged) - 1] .= ', ' . $unit;
continue;
}
$merged[] = $unit;
}
return $merged;
}
private static function hasInitials($unit)
{
return (bool)preg_match('/\p{Lu}\./u', $unit) || (bool)preg_match('/\s\p{Lu}{1,4}$/u', $unit);
}
/**
* 是否只是名/缩写(属于上一个姓):每个词都是首字母大写,且至少有一个是"单字母 + 点"
* "A. B." / "H. Karl." → 是;"Jones AC" / "Bruce Alberts" → 否
*/
private static function looksLikeGivenName($unit)
{
$unit = trim((string)$unit);
if ($unit === '') {
return false;
}
// 末位作者的名常被上游去掉尾点,"K." 会变成 "K"
if (preg_match('/^\p{Lu}\.?$/u', $unit)) {
return true;
}
if (!preg_match('/\b\p{Lu}\./u', $unit)) {
return false;
}
foreach (preg_split('/\s+/u', $unit) as $word) {
$word = trim($word, " .");
if ($word === '') {
continue;
}
if (!preg_match('/^\p{Lu}[\p{L}]*$/u', $word)) {
return false;
}
}
return true;
}
/**
* 判定要偏严:机构名里的逗号片段("Department of Health" / "Education")不能算人名,
* 否则 "Department of Health, Education, and Welfare" 会被当成三位作者
*/
private static function looksLikePerson($unit)
{
$unit = trim(preg_replace('/\s+/u', ' ', str_replace(',', ' ', (string)$unit)));
if ($unit === '') {
return false;
}
// 中文姓名
if (preg_match('/^[\x{4e00}-\x{9fff}·]{2,10}$/u', $unit)) {
return true;
}
// 以缩写结尾:"Smith AB" / "van den Berg AB"
if (preg_match('/^\p{Lu}.*\s\p{Lu}{1,4}$/u', $unit)) {
return true;
}
// 带点的名缩写:"Smith A. B." / "Butcher H. Karl."
if (preg_match('/\b\p{Lu}\./u', $unit)) {
return true;
}
// 两个词的全名:"Bruce Alberts"
return (bool)preg_match('/^\p{Lu}[\p{L}\'\x{2019}\-]+\s+\p{Lu}[\p{L}\'\x{2019}\-]+$/u', $unit);
}
private static function normalize($author)
{
$author = trim((string)$author);
$author = str_replace(['', '', '', ' '], [',', ';', '.', ' '], $author);
$author = preg_replace('/\s+/u', ' ', $author);
return trim($author, " ,;");
}
}