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

1from datetime import date 

2from typing import List, Optional 

3 

4from presidio_analyzer import Pattern, PatternRecognizer 

5 

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 

9 

10 

11class SgUenRecognizer(PatternRecognizer): 

12 """ 

13 Recognize Singapore UEN (Unique Entity Number) using regex. 

14 

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 """ 

20 

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 ] 

28 

29 CONTEXT = ["uen", "unique entity number", "business registration", "ACRA"] 

30 

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 } 

79 

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 ) 

97 

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. 

101 

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 """ 

106 

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) 

116 

117 return False 

118 

119 @staticmethod 

120 def validate_uen_format_a(uen: str) -> bool: 

121 """ 

122 Validate the UEN format A using checksum. 

123 

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] 

129 

130 weighted_sum = sum( 

131 int(n) * w for n, w in zip(uen[:-1], SgUenRecognizer.UEN_FORMAT_A_WEIGHT) 

132 ) 

133 

134 checksum = SgUenRecognizer.UEN_FORMAT_A_ALPHABET[weighted_sum % 11] 

135 

136 return check_digit == checksum 

137 

138 @staticmethod 

139 def validate_uen_format_b(uen: str) -> bool: 

140 """ 

141 Validate the UEN format B using checksum. 

142 

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]) 

149 

150 # Check if the year of registration is not in the future 

151 if year_of_registration > date.today().year: 

152 return False 

153 

154 weighted_sum = sum( 

155 int(n) * w for n, w in zip(uen[:-1], SgUenRecognizer.UEN_FORMAT_B_WEIGHT) 

156 ) 

157 

158 checksum = SgUenRecognizer.UEN_FORMAT_B_ALPHABET[weighted_sum % 11] 

159 

160 return check_digit == checksum 

161 

162 @staticmethod 

163 def validate_uen_format_c(uen: str) -> bool: 

164 """ 

165 Validate the UEN format C using checksum. 

166 

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] 

172 

173 if uen[0] not in SgUenRecognizer.UEN_FORMAT_C_PREFIX: 

174 return False 

175 

176 entity_type = uen[3:5] 

177 

178 if entity_type not in SgUenRecognizer.UEN_FORMAT_C_ENTITY_TYPE: 

179 return False 

180 

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 ) 

185 

186 checksum = SgUenRecognizer.UEN_FORMAT_C_ALPHABET[(weighted_sum - 5) % 11] 

187 

188 return check_digit == checksum