Coverage for presidio_analyzer / predefined_recognizers / country_specific / singapore / sg_uen_recognizer.py: 92%
52 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-29 09:03 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-29 09:03 +0000
1from datetime import date
2from typing import List, Optional
4from presidio_analyzer import Pattern, PatternRecognizer
6# This class includes references to an UEN checksum validation implementation
7# written in Javascript which can be found at:
8# https://gist.github.com/mervintankw/90d5660c6ab03a83ddf77fa8199a0e52
11class SgUenRecognizer(PatternRecognizer):
12 """
13 Recognize Singapore UEN (Unique Entity Number) using regex.
15 :param patterns: List of patterns to be used by this recognizer
16 :param context: List of context words to increase confidence in detection
17 :param supported_language: Language this recognizer supports
18 :param supported_entity: The entity this recognizer can detect
19 """
21 PATTERNS = [
22 Pattern(
23 "UEN (low)",
24 r"\b\d{8}[A-Z]\b|\b\d{9}[A-Z]\b|\b(T|S)\d{2}[A-Z]{2}\d{4}[A-Z]\b",
25 0.3,
26 )
27 ]
29 CONTEXT = ["uen", "unique entity number", "business registration", "ACRA"]
31 UEN_FORMAT_A_WEIGHT = (10, 4, 9, 3, 8, 2, 7, 1)
32 UEN_FORMAT_A_ALPHABET = "XMKECAWLJDB"
33 UEN_FORMAT_B_WEIGHT = (10, 8, 6, 4, 9, 7, 5, 3, 1)
34 UEN_FORMAT_B_ALPHABET = "ZKCMDNERGWH"
35 UEN_FORMAT_C_WEIGHT = (4, 3, 5, 3, 10, 2, 2, 5, 7)
36 UEN_FORMAT_C_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWX0123456789"
37 UEN_FORMAT_C_PREFIX = {"T", "S", "R"}
38 UEN_FORMAT_C_ENTITY_TYPE = {
39 "LP",
40 "LL",
41 "FC",
42 "PF",
43 "RF",
44 "MQ",
45 "MM",
46 "NB",
47 "CC",
48 "CS",
49 "MB",
50 "FM",
51 "GS",
52 "DP",
53 "CP",
54 "NR",
55 "CM",
56 "CD",
57 "MD",
58 "HS",
59 "VH",
60 "CH",
61 "MH",
62 "CL",
63 "XL",
64 "CX",
65 "HC",
66 "RP",
67 "TU",
68 "TC",
69 "FB",
70 "FN",
71 "PA",
72 "PB",
73 "SS",
74 "MC",
75 "SM",
76 "GA",
77 "GB",
78 }
80 def __init__(
81 self,
82 patterns: Optional[List[Pattern]] = None,
83 context: Optional[List[str]] = None,
84 supported_language: str = "en",
85 supported_entity: str = "SG_UEN",
86 name: Optional[str] = None,
87 ):
88 patterns = patterns if patterns else self.PATTERNS
89 context = context if context else self.CONTEXT
90 super().__init__(
91 supported_entity=supported_entity,
92 patterns=patterns,
93 context=context,
94 supported_language=supported_language,
95 name=name,
96 )
98 def validate_result(self, pattern_text: str) -> Optional[bool]:
99 """
100 Validate the pattern logic e.g., by running checksum on a detected pattern.
102 :param pattern_text: the text to validated.
103 Only the part in text that was detected by the regex engine
104 :return: A bool indicating whether the validation was successful.
105 """
107 if len(pattern_text) == 9:
108 # Checksum validation for UEN format A
109 return self.validate_uen_format_a(pattern_text)
110 elif len(pattern_text) == 10 and pattern_text[0].isalpha():
111 # Checksum validation for UEN format C
112 return self.validate_uen_format_c(pattern_text)
113 elif len(pattern_text) == 10:
114 # Checksum validation for UEN format B
115 return self.validate_uen_format_b(pattern_text)
117 return False
119 @staticmethod
120 def validate_uen_format_a(uen: str) -> bool:
121 """
122 Validate the UEN format A using checksum.
124 :param uen: The UEN to validate.
125 :return: True if the UEN is valid according to its respective
126 format, False otherwise.
127 """
128 check_digit = uen[-1]
130 weighted_sum = sum(
131 int(n) * w for n, w in zip(uen[:-1], SgUenRecognizer.UEN_FORMAT_A_WEIGHT)
132 )
134 checksum = SgUenRecognizer.UEN_FORMAT_A_ALPHABET[weighted_sum % 11]
136 return check_digit == checksum
138 @staticmethod
139 def validate_uen_format_b(uen: str) -> bool:
140 """
141 Validate the UEN format B using checksum.
143 :param uen: The UEN to validate.
144 :return: True if the UEN is valid according to its respective
145 format, False otherwise.
146 """
147 check_digit = uen[-1]
148 year_of_registration = int(uen[0:4])
150 # Check if the year of registration is not in the future
151 if year_of_registration > date.today().year:
152 return False
154 weighted_sum = sum(
155 int(n) * w for n, w in zip(uen[:-1], SgUenRecognizer.UEN_FORMAT_B_WEIGHT)
156 )
158 checksum = SgUenRecognizer.UEN_FORMAT_B_ALPHABET[weighted_sum % 11]
160 return check_digit == checksum
162 @staticmethod
163 def validate_uen_format_c(uen: str) -> bool:
164 """
165 Validate the UEN format C using checksum.
167 :param uen: The UEN to validate.
168 :return: True if the UEN is valid according to its respective
169 format, False otherwise.
170 """
171 check_digit = uen[-1]
173 if uen[0] not in SgUenRecognizer.UEN_FORMAT_C_PREFIX:
174 return False
176 entity_type = uen[3:5]
178 if entity_type not in SgUenRecognizer.UEN_FORMAT_C_ENTITY_TYPE:
179 return False
181 weighted_sum = sum(
182 SgUenRecognizer.UEN_FORMAT_C_ALPHABET.index(n) * w
183 for n, w in zip(uen[:-1], SgUenRecognizer.UEN_FORMAT_C_WEIGHT)
184 )
186 checksum = SgUenRecognizer.UEN_FORMAT_C_ALPHABET[(weighted_sum - 5) % 11]
188 return check_digit == checksum