parse static method

Map<String, dynamic> parse(
  1. String text, {
  2. DateTime? now,
})

Implementation

static Map<String, dynamic> parse(String text, {DateTime? now}) {
  final lines = normalize(text);
  if (lines.length != 2 || lines.any((line) => line.length != 44)) {
    throw const FormatException('需要两行机读码,每行 44 个字符。');
  }
  final a = lines[0], b = lines[1];
  if (!RegExp(r'^P[A-Z<][A-Z<]{3}[A-Z<]{39}$').hasMatch(a) ||
      !RegExp(r'^[A-Z0-9<]{44}$').hasMatch(b)) {
    throw const FormatException('不是有效的 TD3 护照机读码字符格式。');
  }
  Map<String, dynamic> check(
    String label,
    String value,
    String actual, {
    bool optional = false,
  }) => {
    'label': label,
    'expected': checkDigit(value),
    'actual': actual,
    'valid':
        checkDigit(value) == actual ||
        (optional && RegExp(r'^<+$').hasMatch(value) && actual == '<'),
  };
  final checks = [
    check('护照号码', b.substring(0, 9), b[9]),
    check('出生日期', b.substring(13, 19), b[19]),
    check('有效期', b.substring(21, 27), b[27]),
    check('可选数据', b.substring(28, 42), b[42], optional: true),
    check(
      '综合校验',
      b.substring(0, 10) + b.substring(13, 20) + b.substring(21, 43),
      b[43],
    ),
  ];
  String clean(String value) =>
      value.replaceAll('<', ' ').trim().replaceAll(RegExp(' +'), ' ');
  final nameField = a.substring(5);
  // Single fillers separate components; repeated fillers after the surname
  // delimiter only pad the end. Reject later letters instead of removing them.
  final nameValid = RegExp(
    r'^[A-Z]+(?:<[A-Z]+)*(?:<<[A-Z]+(?:<[A-Z]+)*)?<*$',
  ).hasMatch(nameField);
  final separator = nameField.indexOf('<<');
  final surname = separator < 0
      ? nameField
      : nameField.substring(0, separator);
  final givenNames = separator < 0 ? '' : nameField.substring(separator + 2);
  final current = (now ?? DateTime.now()).toUtc();
  final birthDate = _date(b.substring(13, 19), true, current);
  final expiryDate = _date(b.substring(21, 27), false, current);
  final structureValid =
      nameValid &&
      RegExp(r'^[A-Z<]{3}$').hasMatch(b.substring(10, 13)) &&
      RegExp(r'^[MF<]$').hasMatch(b[20]) &&
      birthDate != null &&
      expiryDate != null &&
      clean(surname).isNotEmpty &&
      clean(b.substring(0, 9)).isNotEmpty;
  final formatValid =
      structureValid &&
      knownMrzCountryCodes.contains(a.substring(2, 5)) &&
      knownMrzCountryCodes.contains(b.substring(10, 13));
  final identityChecked =
      structureValid && checks.every((c) => c['valid'] == true);
  return {
    'documentType': a.substring(0, 2).replaceAll('<', ''),
    'issuingCountry': a.substring(2, 5),
    'surname': clean(surname),
    'givenNames': clean(givenNames),
    'passportNumber': b.substring(0, 9).replaceAll('<', ''),
    'nationality': b.substring(10, 13),
    'birthDate': birthDate,
    'sex': b[20] == '<' ? 'X' : b[20],
    'expiryDate': expiryDate,
    'rawBirthDate': b.substring(13, 19),
    'rawExpiryDate': b.substring(21, 27),
    'checks': checks,
    'formatValid': formatValid,
    'identityChecked': identityChecked,
    'valid': formatValid && checks.every((c) => c['valid'] == true),
    'dateCenturyInferred': true,
    'rawMrz': lines,
    'warnings': [
      'MRZ 日期只有两位年份,世纪按当前日期推断;请与护照版面核对。',
      '校验位不覆盖姓名和全部字段,也不验证证件真伪。',
    ],
  };
}