mainegeo.elections
Functions for parsing Maine Secretary of State election reporting units.
The Maine SoS uses reporting units that provide the following challenges for parsing:
- Reporting units may be composed of one or several towns.
- Reporting units may include a non-reporting registration town.
- Reporting units may include an unspecified group of all townships that register at a particular town, or in a particular county.
- Delimiters, indicators of reporting vs. registration, and indicators of township aliases are not standard over time.
This module provides methods for parsing Maine election result strings into consistent objects, extracting reporting and registration towns, and standardizing the format of unspecified town groups.
The entry point methods of this module can all be run on a delimited result string containing multiple towns.
Consider the following raw election result strings:
T12/R13 & T9/R8 WELS (ASHLAND)SHERMAN (AND BENEDICTA & SILVER RIDGE TWP)BERRY/CATHANCE/MARION TWPS (EAST MACHIAS)BARNARD TWP, EBEEMEE TWP (T5 R9 NWP), T4 R9 NWP TWPMOUNT CHASE -- T5 R7 TWPISLAND FALLS -- T4-R3 TWPDOVER-FOXCROFT
These examples display huge variation in how formatting is used to convey information.
Parentheses are used in three different ways in these examples:
- In
T12/R13 & T9/R8 WELS (ASHLAND), parentheses convey thatAshlandis a non-reporting registration town. - In
EBEEMEE TWP (T5 R9 NWP), parentheses convey thatT5 R9 NWPis an alias ofEBEEMEE TWP. - In
SHERMAN (AND BENEDICTA & SILVER RIDGE TWP), parentheses convey that this reporting unit includes all ofSHERMAN,BENEDICTA, andSILVER RIDGE, and that all three voted inSHERMAN.
Ampersands, commas, and forward slashes are all used interchangeably to delimit geographies within a string:
BARNARD TWP, EBEEMEE TWP (T5 R9 NWP), T4 R9 NWP TWPis delimited with commas.BERRY/CATHANCE/MARION TWPS (EAST MACHIAS)is delimited with forward slashes (/).SHERMAN (AND BENEDICTA & SILVER RIDGE TWP)contains two different delimiters--the ampersand character (&) and the substringAND.
Forward slashes are sometimes used as delimiters, but in T12/R13 and
T9/R8 WELS they are used to separate township and range designators. Each of
T12/R13 and T9/R8 WELS is a single township, correctly written as T12 R13
and T9 R8 WELS.
Hyphens and parentheses are used interchangeably to indicate a non-reporting registration town:
- In
MOUNT CHASE -- T5 R7 TWP,MOUNT CHASEis the registration town andT5 R7 TWPis the reporting town.MOUNT CHASEis not included in the vote totals for this reporting unit. - In
BERRY/CATHANCE/MARION TWPS (EAST MACHIAS),EAST MACHIASis the registration town andBERRY TWP,CATHANCE TWP, andMARION TWPare all reporting towns.EAST MACHIASis not included in the vote totals for this reporting unit.
However, both characters are also used in other ways (see above).
Single hyphens occur in the canonical names of towns, such as
DOVER-FOXCROFT, and are sometimes used non-canonically to separate township
and range designators, as in the case of T4-R3 TWP (correctly written as T4 R3).
Ampersands also occur in the canonical names of some townships,
e.g. King & Bartlett Twp.
Examples:
Example 1
>>> raw_string = 'T12/R13 & T9/R8 WELS (ASHLAND)'Variations present:
- Ampersand (
&) is used as a token boundary- parentheses (
()) indicate a non-reporting registration town- Forward slash (
/), a character that sometimes serves as a token boundary, is used non-canonically to separate the township and range designators in a township name>>> result = ResultString(raw_string) >>> result.normalized_string 'T12 R13, T9 R8 WELS (ASHLAND)' >>> result.reporting_town_names ['T12 R13', 'T9 R8 WELS'] >>> result.registration_town_names ['ASHLAND']Example 2
>>> raw_string = 'SHERMAN (AND BENEDICTA & SILVER RIDGE TWP) 'Variations present:
- Ampersand (
&) and the substringANDare both used as token boundaries- Parentheses (
()) used to group part of the reporting unit- Trailing whitespace
>>> result = ResultString(raw_string) >>> result.normalized_string 'SHERMAN, BENEDICTA, SILVER RIDGE TWP' >>> result.reporting_town_names ['SHERMAN', 'BENEDICTA', 'SILVER RIDGE TWP'] >>> result.registration_town_names []Example 3
>>> raw_string = 'BERRY/CATHANCE/MARION TWPS (EAST MACHIAS)'Variations present:
- Forward slash (
/) used as token boundary- Parentheses (
()) used to indicate non-reporting registration town>>> result = ResultString(raw_string) >>> result.normalized_string 'BERRY, CATHANCE, MARION TWPS (EAST MACHIAS)' >>> result.reporting_town_names ['BERRY', 'CATHANCE', 'MARION TWPS'] >>> result.registration_town_names ['EAST MACHIAS']Note that the plural 'TWPS' is left unaltered. ResultString operations are not concerned with what real-world geography or geographies a token represents (e.g. whether MARION TWPS is a variation of MARION TWP or group of townships that vote at MARION, whether MARION TWP is even a valid town name, etc).
Example 4
>>> raw_string = 'BARNARD TWP, EBEEMEE TWP (T5 R9 NWP), T4 R9 NWP TWP'Variations present:
- Parentheses (
()) used to indicate township alias, not registration town>>> result = ResultString(raw_string) >>> result.normalized_string 'BARNARD TWP, EBEEMEE TWP (T5 R9 NWP), T4 R9 NWP TWP' >>> result.reporting_town_names ['BARNARD TWP', 'EBEEMEE TWP T5 R9 NWP', 'T4 R9 NWP'] >>> result.registration_town_names []Example 5
>>> raw_string = 'MOUNT CHASE -- T5 R7 TWP'Variations present:
- Double hyphen (
--) used to indicate non-reporting registration town>>> result = ResultString(raw_string) >>> result.normalized_string 'MOUNT CHASE--T5 R7 TWP' >>> result.reporting_town_names ['T5 R7'] >>> result.registration_town_names ['MOUNT CHASE']Example 6
>>> raw_string = 'ISLAND FALLS -- T4-R3 TWP'Variations present:
- Double hyphen (
--) used to indicate non-reporting registration town- Hyphen, (
-), a character that sometimes serves as a token boundary, is used non-canonically to separate the township and range designators in a township name>>> result = ResultString(raw_string) >>> result.normalized_string 'ISLAND FALLS--T4 R3 TWP' >>> result.reporting_town_names ['T4 R3'] >>> result.registration_town_names ['ISLAND FALLS']Example 7
>>> raw_string = 'DOVER-FOXCROFT'Variations present:
- Hyphen, (
-), a character that sometimes indicates a registration town, is used in the canonical name for a town>>> result = ResultString('DOVER-FOXCROFT') >>> result.normalized_string 'DOVER-FOXCROFT' >>> result.reporting_town_names ['DOVER-FOXCROFT'] >>> result.registration_town_names []
1"""Functions for parsing Maine Secretary of State election reporting units. 2 3The Maine SoS uses reporting units that provide the following challenges for parsing: 4 1. Reporting units may be composed of one or several towns. 5 2. Reporting units may include a non-reporting registration town. 6 3. Reporting units may include an unspecified group of all townships that register at a 7 particular town, or in a particular county. 8 4. Delimiters, indicators of reporting vs. registration, and indicators of township aliases 9 are not standard over time. 10 11This module provides methods for parsing Maine election result strings into consistent objects, 12extracting reporting and registration towns, and standardizing the format of unspecified town groups. 13 14The entry point methods of this module can all be run on a delimited result string containing 15multiple towns. 16 17Consider the following raw election result strings: 18 * `T12/R13 & T9/R8 WELS (ASHLAND)` 19 * `SHERMAN (AND BENEDICTA & SILVER RIDGE TWP) ` 20 * `BERRY/CATHANCE/MARION TWPS (EAST MACHIAS)` 21 * `BARNARD TWP, EBEEMEE TWP (T5 R9 NWP), T4 R9 NWP TWP` 22 * `MOUNT CHASE -- T5 R7 TWP` 23 * `ISLAND FALLS -- T4-R3 TWP` 24 * `DOVER-FOXCROFT` 25 26These examples display huge variation in how formatting is used to convey information. 27 28 29Parentheses are used in three different ways in these examples: 30 31* In `T12/R13 & T9/R8 WELS (ASHLAND)`, parentheses convey that `Ashland` 32 is a non-reporting registration town. 33* In `EBEEMEE TWP (T5 R9 NWP)`, parentheses convey that `T5 R9 NWP` is 34 an alias of `EBEEMEE TWP`. 35* In `SHERMAN (AND BENEDICTA & SILVER RIDGE TWP)`, parentheses convey that 36 this reporting unit includes all of `SHERMAN`, `BENEDICTA`, and `SILVER RIDGE`, 37 and that all three voted in `SHERMAN`. 38 39 40Ampersands, commas, and forward slashes are all used interchangeably to delimit 41geographies within a string: 42 43* `BARNARD TWP, EBEEMEE TWP (T5 R9 NWP), T4 R9 NWP TWP` is delimited with commas. 44* `BERRY/CATHANCE/MARION TWPS (EAST MACHIAS)` is delimited with forward slashes (`/`). 45* `SHERMAN (AND BENEDICTA & SILVER RIDGE TWP) ` contains two different 46 delimiters--the ampersand character (`&`) and the substring `AND`. 47 48 49Forward slashes are sometimes used as delimiters, but in `T12/R13` and 50`T9/R8 WELS` they are used to separate township and range designators. Each of 51`T12/R13` and `T9/R8 WELS` is a single township, correctly written as `T12 R13` 52and `T9 R8 WELS`. 53 54 55Hyphens and parentheses are used interchangeably to indicate 56a non-reporting registration town: 57 58* In `MOUNT CHASE -- T5 R7 TWP`, `MOUNT CHASE` is the registration town and 59 `T5 R7 TWP` is the reporting town. `MOUNT CHASE` is not included in the vote totals 60 for this reporting unit. 61* In `BERRY/CATHANCE/MARION TWPS (EAST MACHIAS)`, `EAST MACHIAS` is the registration 62 town and `BERRY TWP`, `CATHANCE TWP`, and `MARION TWP` are all reporting towns. 63 `EAST MACHIAS` is not included in the vote totals for this reporting unit. 64 65However, both characters are also used in other ways (see above). 66 67 68Single hyphens occur in the canonical names of towns, such as 69`DOVER-FOXCROFT`, and are sometimes used non-canonically to separate township 70and range designators, as in the case of `T4-R3 TWP` (correctly written as `T4 R3`). 71 72 73Ampersands also occur in the canonical names of some townships, 74e.g. `King & Bartlett Twp`. 75 76 77Examples: 78 **Example 1** 79 >>> raw_string = 'T12/R13 & T9/R8 WELS (ASHLAND)' 80 81 Variations present: 82 83 * Ampersand (`&`) is used as a token boundary 84 * parentheses (`()`) indicate a non-reporting registration town 85 * Forward slash (`/`), a character that sometimes serves as a token boundary, 86 is used non-canonically to separate the township and range designators in a 87 township name 88 89 >>> result = ResultString(raw_string) 90 >>> result.normalized_string 91 'T12 R13, T9 R8 WELS (ASHLAND)' 92 >>> result.reporting_town_names 93 ['T12 R13', 'T9 R8 WELS'] 94 >>> result.registration_town_names 95 ['ASHLAND'] 96 97 98 **Example 2** 99 >>> raw_string = 'SHERMAN (AND BENEDICTA & SILVER RIDGE TWP) ' 100 101 Variations present: 102 103 * Ampersand (`&`) and the substring `AND` are both used as token boundaries 104 * Parentheses (`()`) used to group part of the reporting unit 105 * Trailing whitespace 106 107 >>> result = ResultString(raw_string) 108 >>> result.normalized_string 109 'SHERMAN, BENEDICTA, SILVER RIDGE TWP' 110 >>> result.reporting_town_names 111 ['SHERMAN', 'BENEDICTA', 'SILVER RIDGE TWP'] 112 >>> result.registration_town_names 113 [] 114 115 116 **Example 3** 117 >>> raw_string = 'BERRY/CATHANCE/MARION TWPS (EAST MACHIAS)' 118 119 Variations present: 120 121 * Forward slash (`/`) used as token boundary 122 * Parentheses (`()`) used to indicate non-reporting registration town 123 124 >>> result = ResultString(raw_string) 125 >>> result.normalized_string 126 'BERRY, CATHANCE, MARION TWPS (EAST MACHIAS)' 127 >>> result.reporting_town_names 128 ['BERRY', 'CATHANCE', 'MARION TWPS'] 129 >>> result.registration_town_names 130 ['EAST MACHIAS'] 131 132 Note that the plural 'TWPS' is left unaltered. ResultString operations are 133 not concerned with what real-world geography or geographies a token represents 134 (e.g. whether MARION TWPS is a variation of MARION TWP or group of townships 135 that vote at MARION, whether MARION TWP is even a valid town name, etc). 136 137 138 **Example 4** 139 >>> raw_string = 'BARNARD TWP, EBEEMEE TWP (T5 R9 NWP), T4 R9 NWP TWP' 140 141 Variations present: 142 143 * Parentheses (`()`) used to indicate township alias, not registration 144 town 145 146 >>> result = ResultString(raw_string) 147 >>> result.normalized_string 148 'BARNARD TWP, EBEEMEE TWP (T5 R9 NWP), T4 R9 NWP TWP' 149 >>> result.reporting_town_names 150 ['BARNARD TWP', 'EBEEMEE TWP T5 R9 NWP', 'T4 R9 NWP'] 151 >>> result.registration_town_names 152 [] 153 154 155 **Example 5** 156 >>> raw_string = 'MOUNT CHASE -- T5 R7 TWP' 157 158 Variations present: 159 160 * Double hyphen (`--`) used to indicate non-reporting registration 161 town 162 163 >>> result = ResultString(raw_string) 164 >>> result.normalized_string 165 'MOUNT CHASE--T5 R7 TWP' 166 >>> result.reporting_town_names 167 ['T5 R7'] 168 >>> result.registration_town_names 169 ['MOUNT CHASE'] 170 171 172 **Example 6** 173 >>> raw_string = 'ISLAND FALLS -- T4-R3 TWP' 174 175 Variations present: 176 177 * Double hyphen (`--`) used to indicate non-reporting registration 178 town 179 * Hyphen, (`-`), a character that sometimes serves as a token boundary, 180 is used non-canonically to separate the township and range designators 181 in a township name 182 183 >>> result = ResultString(raw_string) 184 >>> result.normalized_string 185 'ISLAND FALLS--T4 R3 TWP' 186 >>> result.reporting_town_names 187 ['T4 R3'] 188 >>> result.registration_town_names 189 ['ISLAND FALLS'] 190 191 192 **Example 7** 193 >>> raw_string = 'DOVER-FOXCROFT' 194 195 Variations present: 196 197 * Hyphen, (`-`), a character that sometimes indicates a registration town, 198 is used in the canonical name for a town 199 200 >>> result = ResultString('DOVER-FOXCROFT') 201 >>> result.normalized_string 202 'DOVER-FOXCROFT' 203 >>> result.reporting_town_names 204 ['DOVER-FOXCROFT'] 205 >>> result.registration_town_names 206 [] 207""" 208 209__docformat__ = 'google' 210 211__all__ = [ 212 'ResultString', 213 'ReportingUnit' 214] 215 216import re 217from dataclasses import dataclass, asdict 218from functools import cached_property 219from typing import List, Type 220from itertools import filterfalse 221 222from mainegeo._vendor import replace_all, normalize_whitespace, chain_operations 223from mainegeo.matching import get_town_database 224from mainegeo.entities import County, Cousub 225from mainegeo.townships import ( 226 clean_code, 227 clean_codes, 228 clean_town, 229 is_unnamed_township, 230 has_alias, 231 extract_alias 232) 233from mainegeo.patterns import ( 234 KNOWN_TYPOS, 235 AMBIGUOUS_GROUPS, 236 DROP_CHARACTERS_PATTERN, 237 STANDARD_DELIMITER, 238 NONSTANDARD_DELIMITER_PATTERN, 239 ORPHAN_PARENTHESIS_PATTERN, 240 REGISTRATION_PATTERN, 241 UNSPECIFIED_FLAG, 242 STANDARD_FLAG, 243 MULTI_COUNTY_PATTERN, 244 MULTI_COUNTY_FORMAT, 245 MULTI_COUNTY_REGISTRATION_TOWNS, 246 PLURAL, 247 SINGULAR, 248 PLURAL_PATTERN, 249 SINGULAR_PATTERN, 250 VALID_AMPERSANDS_PATTERN, 251 FORMATTED_GROUP_PATTERN 252) 253 254@dataclass 255class ResultString: 256 """ 257 A string containing one or more geographies from raw election results. 258 259 This dataclass performs initial normalization and parsing operations on a raw 260 election result string. It owns operations that are fully agnostic to which 261 geographies the tokens in the string represent. 262 263 These operations normalize variations in meaningful formatting (including 264 whitespace, delimiters, and other punctuation) so that two types of parsing can occur: 265 266 1. The splitting of result strings into a list of tokens, each 267 one representing a single geography. 268 2. The separation of tokens representing reporting geographies from tokens 269 representing non-reporting registration towns. 270 271 Args: 272 raw_string: The raw string representation of the reporting unit 273 """ 274 raw_string: str 275 276 @cached_property 277 def exists(self) -> bool: 278 if self.raw_string is None: 279 return False 280 elif len(self.raw_string) > 5: 281 return True 282 elif re.search(r'\w', self.raw_string): 283 return True 284 else: 285 return False 286 287 @cached_property 288 def normalized_string(self) -> str: 289 """ 290 Result string with capitalization, whitespace and delimiters normalized. 291 292 Errors entered in `mainegeo.lookups.Overrides.known_typos` and 293 `mainegeo.lookups.Overrides.ambiguous_groups` are also repaired. 294 295 Example: 296 >>> result = ResultString('FORT KENT/BIG TWENTY TWP/ T15 R15 WELS') 297 >>> result.normalized_string 298 'FORT KENT, BIG TWENTY TWP, T15 R15 WELS' 299 300 >>> result = ResultString('T12/R13 WELS/T9 R8 WELS') 301 >>> result.normalized_string 302 'T12 R13 WELS, T9 R8 WELS' 303 304 >>> result = ResultString('T10 SD TWP (CHERRYFIELD, FRANKLIN & MILBRIDGE)') 305 >>> result.normalized_string 306 'T10 SD TWP (CHERRYFIELD, FRANKLIN, MILBRIDGE)' 307 """ 308 initial_cleanup = [ 309 str.upper 310 , ResultString._fix_known_typos 311 , ResultString._rename_ambiguous_groups 312 , ResultString._drop_non_meaningful_chars 313 , clean_codes 314 , ResultString._normalize_delimiters 315 , normalize_whitespace 316 ] 317 if self.exists: 318 return chain_operations(self.raw_string, initial_cleanup) 319 320 @cached_property 321 def registration_town_names(self) -> List[str]: 322 """ 323 List of registration town names extracted from result string. 324 325 Note: 326 Splits strings by package-level `STANDARD_DELIMITER`. 327 328 Example: 329 >>> result = ResultString('MOUNT CHASE -- T5 R7 TWP') 330 >>> result.registration_town_names 331 ['MOUNT CHASE'] 332 333 >>> result = ResultString('T7 SD TWP (STEUBEN)') 334 >>> result.registration_town_names 335 ['STEUBEN'] 336 337 >>> result = ResultString('CROSS LAKE TWP (T17 R5)') 338 >>> result.registration_town_names 339 [] 340 341 >>> result = ResultString('ARGYLE TWP (ALTON, EDINBURG)') 342 >>> result.registration_town_names 343 ['ALTON', 'EDINBURG'] 344 """ 345 if self._registration_town_substring is None: 346 return [] 347 else: 348 reg_towns = self._registration_town_substring.split(STANDARD_DELIMITER) 349 return list(map(clean_town, reg_towns)) 350 351 @cached_property 352 def reporting_town_names(self) -> List[str]: 353 """ 354 Extract list of reporting town names from result string. 355 356 Note: 357 Drops parentheses around township alias, but does not remove alias. 358 359 Args: 360 result_str: Delimited result string with one or more towns or townships 361 362 Returns: 363 List: Reporting towns with formatting identifiers stripped 364 365 Example: 366 >>> ResultString('MOUNT CHASE--T5 R7 TWP').reporting_town_names 367 ['T5 R7'] 368 369 >>> ResultString('HERSEYTOWN, SOLDIERTOWN TWPS (MEDWAY)').reporting_town_names 370 ['HERSEYTOWN', 'SOLDIERTOWN TWPS'] 371 372 >>> ResultString('ARGYLE TWP (ALTON, EDINBURG)').reporting_town_names 373 ['ARGYLE TWP'] 374 375 >>> ResultString('BARNARD TWP, EBEEMEE TWP (T5 R9 NWP), T4 R9 NWP TWP').reporting_town_names 376 ['BARNARD TWP', 'EBEEMEE TWP T5 R9 NWP', 'T4 R9 NWP'] 377 378 >>> ResultString('BERRY/CATHANCE/MARIONTWPS (EAST MACHIAS)').reporting_town_names 379 ['BERRY', 'CATHANCE', 'MARION TWPS'] 380 """ 381 if self._registration_town_substring is None: 382 reporting_substr = self.normalized_string 383 else: 384 reporting_substr = re.sub(self._registration_town_substring, '', self.normalized_string) 385 386 reporting = reporting_substr.split(STANDARD_DELIMITER) 387 return list(map(clean_town, reporting)) 388 389 @cached_property 390 def _registration_town_substring(self) -> str: 391 """ 392 Registration town substring extracted based on SoS formatting identifiers. 393 394 SoS formatting identifiers include parentheticals and double dashes (--). 395 Parentheticals that are township aliases are ignored. 396 397 Note: 398 Substring includes formatting indicators. 399 400 Raises: 401 ValueError: If result string contains multiple registration town substrings. 402 403 Examples: 404 >>> result = ResultString('MOUNT CHASE -- T5 R7 TWP') 405 >>> result._registration_town_substring 406 'MOUNT CHASE--' 407 408 >>> result = ResultString('T7 SD TWP (STEUBEN)') 409 >>> result._registration_town_substring 410 '(STEUBEN)' 411 412 >>> result = ResultString('ARGYLE TWP (ALTON, EDINBURG)') 413 >>> result._registration_town_substring 414 '(ALTON, EDINBURG)' 415 """ 416 if self.exists: 417 flagged = REGISTRATION_PATTERN.findall(self.normalized_string) 418 substrings = list(filterfalse(is_unnamed_township, flagged)) 419 420 if len(substrings) > 1: 421 raise ValueError(f'Multiple registration town substrings found in result string: {self.normalized_string}') 422 elif len(substrings) == 0: 423 return None 424 else: 425 return substrings[0] 426 427 @staticmethod 428 def _fix_known_typos(result_str: str) -> str: 429 """ 430 Fix known typos in-place in string. 431 432 These typos are true misspellings, which are unique and unlikely to be repeated. 433 They are stored in the KNOWN_TYPOS dict in the patterns module. 434 435 Args: 436 result_str: Delimited result string with one or more towns or townships 437 """ 438 return replace_all(KNOWN_TYPOS, result_str) 439 440 @staticmethod 441 def _rename_ambiguous_groups(result_str: str) -> str: 442 """ 443 Fix ambiguous unspecified group names in-place in string. 444 445 These typos recur from time to time, but follow a general pattern. It simplifies 446 unspecified group name detection significantly if they are corrected early in 447 processing. They are stored in `lookups.Overrides.ambiguous_groups`. 448 449 Args: 450 result_str: Delimited result string with one or more towns or townships 451 452 Example: 453 >>> ResultString._rename_ambiguous_groups('MILLINOCKET -- PISCATAQUIS TWP') 454 'MILLINOCKET -- PISCATAQUIS TWPS' 455 456 >>> ResultString._rename_ambiguous_groups('PENOBSCOT TWPS') 457 'MILLINOCKET PENOBSCOT TWPS' 458 """ 459 return replace_all(AMBIGUOUS_GROUPS, result_str) 460 461 @staticmethod 462 def _drop_non_meaningful_chars(result_str: str) -> str: 463 """ 464 Drop or replace special characters that don't communicate meaningful info. 465 466 Non-meaningful characters include all punctuation characters not found in 467 `mainegeo.patterns.MEANINGFUL_CHARACTERS`. 468 469 Args: 470 result_str: Delimited result string with one or more towns or townships 471 """ 472 result = VALID_AMPERSANDS_PATTERN.sub('AND', normalize_whitespace(result_str)) 473 return DROP_CHARACTERS_PATTERN.sub('', result) 474 475 @staticmethod 476 def _normalize_delimiters(result_str: str) -> str: 477 """ 478 Replace all delimiters in result string with standard delimiter. 479 480 Args: 481 result_str: Delimited result string with one or more towns or townships 482 """ 483 delimit = NONSTANDARD_DELIMITER_PATTERN.sub(STANDARD_DELIMITER, result_str) 484 cleanup = ORPHAN_PARENTHESIS_PATTERN.sub(r'\g<result>', delimit) 485 return cleanup 486 487@dataclass(frozen=True) 488class ResultGeo: 489 name: str 490 county: County 491 strict: bool = False 492 493 @classmethod 494 def from_strings(cls, name: str, county_code: str, strict: bool = False) -> "ResultGeo": 495 return cls( 496 name = name, 497 county = County(code=county_code), 498 strict = strict 499 ) 500 501@dataclass(frozen=True) 502class Municipality(ResultGeo): 503 @cached_property 504 def matched_town(self): 505 return get_town_database().match( 506 self.name, 507 self.county.fips, 508 cleaned = True, 509 strict = self.strict 510 ) 511 512 @property 513 def is_matched(self): 514 return self.matched_town is not None 515 516 @property 517 def canonical_name(self): 518 if self.matched_town: 519 return self.matched_town.name 520 521 @property 522 def consensus_name(self): 523 return self.canonical_name or self.name 524 525 @cached_property 526 def matched_geocode(self) -> str: 527 if self.matched_town: 528 return self.matched_town.geocode 529 530 @cached_property 531 def matched_cousub(self) -> Cousub: 532 return self.matched_town.cousub if self.matched_town else Cousub() 533 534 @cached_property 535 def matched_county(self) -> County: 536 return self.matched_town.county if self.matched_town else County() 537 538 def to_dict(self) -> dict[str]: 539 return { 540 'name': self.consensus_name, 541 'canonical_name': self.canonical_name, 542 'raw_name': self.name, 543 'county': asdict(self.matched_county), 544 'cousub': asdict(self.matched_cousub), 545 'geocode': self.matched_geocode, 546 'is_matched': self.is_matched 547 } 548 549@dataclass(frozen=True) 550class NamedTownship(Municipality): 551 pass 552 553@dataclass(frozen=True) 554class UnnamedTownship(Municipality): 555 @property 556 def has_alias(self): 557 return has_alias(self.name) 558 559 @property 560 def alias(self): 561 return extract_alias(self.name) 562 563 @property 564 def code(self): 565 return clean_code(self.name) 566 567@dataclass(frozen=True) 568class UnspecifiedGroup(ResultGeo): 569 @cached_property 570 def _format_match(self) -> re.Match: 571 return FORMATTED_GROUP_PATTERN.match(self.name) 572 573 @property 574 def is_matched(self) -> bool: 575 return all((self._format_match, self.group_registration_town.is_matched)) 576 577 @property 578 def group_county(self) -> County: 579 county_code = self._format_match.group('cty') or self.county.code 580 return County(code = county_code) 581 582 @property 583 def group_registration_town(self) -> NamedTownship: 584 reg_town_name = self._format_match.group('regtown') 585 return NamedTownship( 586 name = reg_town_name, 587 county = self.county, 588 strict = self.strict 589 ) 590 591 @property 592 def canonical_name(self): 593 regtown = self.group_registration_town.consensus_name 594 595 if regtown.upper() in MULTI_COUNTY_REGISTRATION_TOWNS: 596 county = f' {self.group_county.name} County ' 597 else: 598 county = ' ' 599 600 formatted = f'{STANDARD_FLAG}{county}{UNSPECIFIED_FLAG}' 601 return formatted.title() 602 603 @property 604 def consensus_name(self): 605 return self.canonical_name or self.name 606 607 def to_dict(self): 608 return { 609 'name': self.consensus_name, 610 'canonical_name': self.canonical_name, 611 'raw_name': self.name, 612 'county': asdict(self.group_county), 613 'group_registration_town': self.group_registration_town.to_dict(), 614 'is_matched': self.is_matched 615 } 616 617@dataclass 618class ReportingUnit: 619 """A collection of towns and unspecified groups parsed from a `ResultString`. 620 621 This class performs normalization operations on individual fragments of a 622 delimited string, coerces them into objects representing different geography 623 types, and attempts to match them to known Maine geographies. It provides several 624 options for representing the fully parsed unit as a string or dictionary. 625 626 Args: 627 result_string: A `ResultString` object 628 county: A `County` object 629 strict: True if exception should be raised when a match fails 630 """ 631 result_string: ResultString 632 county: County 633 strict: bool = False 634 635 @classmethod 636 def from_strings( 637 cls, 638 result_str: str, 639 county_code: str, 640 strict: bool = False 641 ) -> "ReportingUnit": 642 """ Factory method to create a fully processed ReportingUnit. 643 644 Examples: 645 >>> result = ReportingUnit.from_strings('PRENTISS TWP (WEBSTER PLT)', 'PEN') 646 >>> result.formatted_string 647 'Prentiss Twp T7 R3 NBPP [Webster Plt]' 648 >>> result.reporting_town_names 649 ['Prentiss Twp T7 R3 NBPP'] 650 >>> result.registration_town_names 651 ['Webster Plt'] 652 >>> result.unspecified_groups 653 [] 654 655 >>> result = ReportingUnit.from_strings('MEDWAY -- GRINDSTONE/SOLDIERTOWN TWPS', 'PEN') 656 >>> result.formatted_string 657 'Grindstone Twp [Medway], Soldiertown Twp T2 R7 WELS [Medway]' 658 >>> result.reporting_town_names 659 ['Grindstone Twp', 'Soldiertown Twp T2 R7 WELS'] 660 >>> result.registration_town_names 661 ['Medway'] 662 663 >>> result = ReportingUnit.from_strings('FRANKLIN/T9 T10 SD TWPS', 'HAN') 664 >>> result.formatted_string 665 'Franklin, T9 SD BPP, T10 SD BPP' 666 >>> result.reporting_town_names 667 ['Franklin', 'T9 SD BPP', 'T10 SD BPP'] 668 >>> result.registration_town_names 669 [] 670 671 >>> result = ReportingUnit.from_strings('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') 672 >>> result.formatted_string 673 'Millinocket, Unspecified Piscataquis County Twps [Millinocket]' 674 >>> result.reporting_town_names 675 ['Millinocket', 'Unspecified Piscataquis County Twps'] 676 >>> result.registration_town_names 677 ['Millinocket'] 678 >>> len(result.unspecified_groups) 679 1 680 681 >>> result = ReportingUnit.from_strings('MILLINOCKET/PIS TWPS/PEN TWPS', 'PEN') 682 >>> result.reporting_town_names 683 ['Millinocket', 'Unspecified Piscataquis County Twps', 'Unspecified Penobscot County Twps'] 684 >>> result.registration_town_names 685 ['Millinocket'] 686 >>> len(result.unspecified_groups) 687 2 688 689 >>> result = ReportingUnit.from_strings('MEDWAY TOWNSHIPS', 'PEN') 690 >>> result.formatted_string 691 'Unspecified Twps [Medway]' 692 >>> result.reporting_town_names 693 ['Unspecified Twps'] 694 >>> result.registration_town_names 695 ['Medway'] 696 >>> len(result.unspecified_groups) 697 1 698 """ 699 unit = cls( 700 result_string = ResultString(result_str), 701 county = County(code = county_code), 702 strict = strict 703 ) 704 return unit 705 706 @property 707 def raw_string(self) -> str: 708 """ 709 Original SoS name for this reporting unit. 710 711 Examples: 712 >>> unit = ReportingUnit.from_strings('WEBSTER PLT -- PRENTISS TWP', 'PEN') 713 >>> unit.raw_string 714 'WEBSTER PLT -- PRENTISS TWP' 715 """ 716 return self.result_string.raw_string 717 718 @cached_property 719 def formatted_string(self) -> str: 720 """ 721 A formatted string representation of this reporting unit. 722 723 Examples: 724 >>> args = ('T12/R13 & T9/R8 WELS (ASHLAND)', 'ARO') 725 >>> ReportingUnit.from_strings(*args).formatted_string 726 'T12 R13 WELS [Ashland], T9 R8 WELS [Ashland]' 727 728 >>> args = ('GRINDSTONE/HERSEYTOWN/SOLDIERTOWN TWP', 'PEN') 729 >>> ReportingUnit.from_strings(*args).formatted_string 730 'Grindstone Twp, Herseytown Twp, Soldiertown Twp T2 R7 WELS' 731 732 >>> args = ('SHERMAN (AND BENEDICTA & SILVER RIDGE TWPS) ', 'ARO') 733 >>> ReportingUnit.from_strings(*args).formatted_string 734 'Sherman, Benedicta Twp, Silver Ridge Twp' 735 736 >>> args = ('JACKMAN TWPS', 'SOM') 737 >>> ReportingUnit.from_strings(*args).formatted_string 738 'Unspecified Twps [Jackman]' 739 740 >>> args = ('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') 741 >>> ReportingUnit.from_strings(*args).formatted_string 742 'Millinocket, Unspecified Piscataquis County Twps [Millinocket]' 743 """ 744 reporting = [ 745 town + f' [{self.registration_string}]' 746 if 747 len(self.registration_town_names) > 0 748 and town not in self.registration_town_names 749 else town 750 for town in self.reporting_town_names 751 ] 752 return STANDARD_DELIMITER.join(reporting) 753 754 @cached_property 755 def reporting_string(self) -> str: 756 """ 757 A formatted string representation of reporting towns in this unit. 758 759 Examples: 760 >>> args = ('WEBSTER PLT -- PRENTISS TWP', 'PEN') 761 >>> ReportingUnit.from_strings(*args).reporting_string 762 'Prentiss Twp T7 R3 NBPP' 763 764 >>> args = ('T12/R13 & T9/R8 WELS (ASHLAND)', 'ARO') 765 >>> ReportingUnit.from_strings(*args).reporting_string 766 'T12 R13 WELS, T9 R8 WELS' 767 768 >>> args = ('JACKMAN TWPS', 'SOM') 769 >>> ReportingUnit.from_strings(*args).reporting_string 770 'Unspecified Twps' 771 772 >>> args = ('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') 773 >>> ReportingUnit.from_strings(*args).reporting_string 774 'Millinocket, Unspecified Piscataquis County Twps' 775 776 >>> args = ('MILLINOCKET/PEN TWPS/PIS TWPS', 'PEN') 777 >>> ReportingUnit.from_strings(*args).reporting_string 778 'Millinocket, Unspecified Penobscot County Twps, Unspecified Piscataquis County Twps' 779 """ 780 return STANDARD_DELIMITER.join(self.reporting_town_names) 781 782 @cached_property 783 def registration_string(self) -> str: 784 """ 785 A formatted string representation of registration towns in this unit. 786 787 Examples: 788 >>> args = ('WEBSTER PLT -- PRENTISS TWP', 'PEN') 789 >>> ReportingUnit.from_strings(*args).registration_string 790 'Webster Plt' 791 792 >>> args = ('WYMAN TWP (CARRABASSETT VALLEY & EUSTIS)', 'FRA') 793 >>> ReportingUnit.from_strings(*args).registration_string 794 'Carrabassett Valley, Eustis' 795 796 >>> args = ('JACKMAN TWPS', 'SOM') 797 >>> ReportingUnit.from_strings(*args).registration_string 798 'Jackman' 799 800 >>> args = ('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') 801 >>> ReportingUnit.from_strings(*args).registration_string 802 'Millinocket' 803 804 >>> args = ('MILLINOCKET/PIS TWPS/PEN TWPS', 'PEN') 805 >>> ReportingUnit.from_strings(*args).registration_string 806 'Millinocket' 807 """ 808 return STANDARD_DELIMITER.join(self.registration_town_names) 809 810 @cached_property 811 def reporting_town_names(self) -> list[str]: 812 """ 813 List of reporting town names. Canonical name if match was found, else raw name. 814 """ 815 return [town.consensus_name for town in self.reporting_towns] 816 817 @cached_property 818 def registration_town_names(self) -> list[str]: 819 """ 820 List of registration town names. Canonical name if match was found, else raw name. 821 """ 822 return [town.consensus_name for town in self.registration_towns] 823 824 @cached_property 825 def registration_towns(self) -> List[NamedTownship]: 826 """ 827 List of registration towns as `NamedTownship` objects. 828 """ 829 towns = set() 830 for name in self.result_string.registration_town_names: 831 regtown = NamedTownship(name, self.county, strict = self.strict) 832 towns.add(regtown) 833 834 for group in self.unspecified_groups: 835 towns.add(group.group_registration_town) 836 837 return sorted(towns, key=lambda town: town.consensus_name) 838 839 @cached_property 840 def reporting_towns(self) -> List[ResultGeo]: 841 """ 842 List of reporting towns, townships and groups as `ResultGeo` child objects. 843 """ 844 towns = [] 845 846 if self.result_string.exists: 847 formatted_reporting_names = ReportingUnit._format_reporting_towns( 848 self.result_string.reporting_town_names, 849 self.result_string.registration_town_names, 850 self.has_unspecified_group 851 ) 852 for name in formatted_reporting_names: 853 ResultClass = ReportingUnit._classify_fragment(name) 854 reporting_town = ResultClass(name, self.county, strict = self.strict) 855 towns.append(reporting_town) 856 857 return towns 858 859 @cached_property 860 def specified_reporting_towns(self) -> List[Municipality]: 861 """ 862 List of reporting units that are not unspecified groups. 863 864 Examples: 865 >>> unit = ReportingUnit.from_strings('WYMAN TWP/SPRING LAKE TWP', 'FRA') 866 >>> [town.name for town in unit.specified_reporting_towns] 867 ['WYMAN TWP', 'SPRING LAKE TWP'] 868 869 >>> unit = ReportingUnit.from_strings('WYMAN TWP (CARRABASSETT VALLEY)', 'FRA') 870 >>> [town.name for town in unit.specified_reporting_towns] 871 ['WYMAN TWP'] 872 873 >>> unit = ReportingUnit.from_strings('MILLINOCKET/TWPS', 'PEN') 874 >>> [town.name for town in unit.specified_reporting_towns] 875 ['MILLINOCKET'] 876 877 >>> unit = ReportingUnit.from_strings('MILLINOCKET TWPS', 'PEN') 878 >>> [town.name for town in unit.specified_reporting_towns] 879 [] 880 """ 881 return [t for t in self.reporting_towns if type(t) != UnspecifiedGroup] 882 883 @cached_property 884 def unspecified_groups(self) -> List[ResultGeo]: 885 """ 886 List of reporting units that are unspecified groups. 887 888 Examples: 889 >>> unit = ReportingUnit.from_strings('MILLINOCKET/PEN TWPS', 'PEN') 890 >>> groups = unit.unspecified_groups 891 >>> [group.name for group in groups] 892 ['UNSPECIFIED MILLINOCKET TWPS [PEN]'] 893 >>> [group.canonical_name for group in groups] 894 ['Unspecified Penobscot County Twps'] 895 >>> [group.group_registration_town.canonical_name for group in groups] 896 ['Millinocket'] 897 """ 898 return [t for t in self.reporting_towns if type(t) == UnspecifiedGroup] 899 900 @cached_property 901 def has_unspecified_group(self) -> bool: 902 """ 903 Return True if the result object includes an unspecified group, else False. 904 905 Examples: 906 >>> unit = ReportingUnit.from_strings('MEDWAY/TOWNSHIPS', 'PEN') 907 >>> unit.has_unspecified_group 908 True 909 910 >>> unit = ReportingUnit.from_strings('ADAMSTOWN/LOWER CUPSUPTIC TWPS (RANGELEY)', 'OXF') 911 >>> unit.has_unspecified_group 912 False 913 914 >>> unit = ReportingUnit.from_strings('MILLINOCKET PISCATAQUIS TWPS', 'PIS') 915 >>> unit.has_unspecified_group 916 True 917 918 >>> unit = ReportingUnit.from_strings('MILLINOCKET/PEN TWPS', 'PEN') 919 >>> unit.has_unspecified_group 920 True 921 922 >>> unit = ReportingUnit.from_strings('LEXINGTON & SPRING LAKE TWPS', 'SOM') 923 >>> unit.has_unspecified_group 924 False 925 """ 926 reporting = self.result_string.reporting_town_names 927 registration = self.result_string.registration_town_names 928 group_name = ' '.join(filter(None, [*registration, *reporting])) 929 930 if len(registration) > 1: 931 return False 932 elif len(reporting) in (1, 2) and UNSPECIFIED_FLAG in reporting: 933 return True 934 elif all(UNSPECIFIED_FLAG in town for town in reporting): 935 return True 936 elif MULTI_COUNTY_PATTERN.match(group_name): 937 return True 938 else: 939 return False 940 941 @cached_property 942 def is_matched(self) -> bool: 943 """True if every named geography in this unit matched a known town.""" 944 if not self.result_string.exists: 945 return False 946 else: 947 tokens = [*self.reporting_towns, *self.registration_towns] 948 return bool(tokens) and all(t.is_matched for t in tokens) 949 950 def to_dict(self) -> dict[str]: 951 return { 952 'raw_str': self.raw_string, 953 'formatted_str': self.formatted_string, 954 'reporting_str': self.reporting_string, 955 'registration_str': self.registration_string, 956 'reporting': { 957 'specified': [town.to_dict() for town in self.specified_reporting_towns], 958 'unspecified': [group.to_dict() for group in self.unspecified_groups] 959 }, 960 'registration': [town.to_dict() for town in self.registration_towns], 961 'county': asdict(self.county), 962 'is_matched': self.is_matched 963 } 964 965 @staticmethod 966 def _format_reporting_towns( 967 reporting_town_names: List[str], 968 registration_town_names: List[str], 969 has_unspecified_group: bool) -> List[str]: 970 """ 971 Apply consistent format to towns and unspecified groups. 972 973 Args: 974 reporting_towns: List of one or more towns 975 registration_towns: List of non-reporting registration towns (if any) 976 has_unspecified_group: True if unit contains unspecified group, else False 977 978 Returns: 979 list: Reporting towns with standard formatting applied. 980 981 Examples: 982 >>> ReportingUnit._format_reporting_towns(['LEXINGTON', 'SPRING LAKE TWPS'], [], False) 983 ['LEXINGTON', 'SPRING LAKE TWP'] 984 985 >>> ReportingUnit._format_reporting_towns(['FRANKLIN', 'TWPS'], [], True) 986 ['FRANKLIN', 'UNSPECIFIED FRANKLIN TWPS'] 987 988 >>> ReportingUnit._format_reporting_towns(['PENOBSCOT TWPS'], ['MILLINOCKET'], True) 989 ['UNSPECIFIED MILLINOCKET TWPS [PEN]'] 990 """ 991 reporting = [ 992 ReportingUnit._format_plural(town, has_unspecified_group) 993 for town in reporting_town_names 994 ] 995 996 if has_unspecified_group: 997 return ReportingUnit._name_unspecified_group(reporting, registration_town_names) 998 else: 999 return reporting 1000 1001 @staticmethod 1002 def _format_plural(town: str, has_unspecified_group: bool) -> str: 1003 """ 1004 Correct errors of pluralization in town or group names. 1005 1006 After this function is used, the presence or absence of a plural in a town name 1007 reliably indicates whether it is an unspecified group. 1008 """ 1009 if has_unspecified_group: 1010 return SINGULAR_PATTERN.sub(PLURAL, town) 1011 else: 1012 return PLURAL_PATTERN.sub(SINGULAR, town) 1013 1014 @staticmethod 1015 def _format_unspecified_group(group_name: str) -> str: 1016 """ 1017 Apply special format to unspecified groups that include a county. 1018 """ 1019 return MULTI_COUNTY_PATTERN.sub(MULTI_COUNTY_FORMAT, group_name) 1020 1021 @staticmethod 1022 def _name_unspecified_group( 1023 reporting_town_names: List[str], 1024 registration_town_names: List[str]) -> List[str]: 1025 """ 1026 Label unspecified groups with their reporting town and a standard 'unspecified' flag. 1027 """ 1028 is_specified_reporting = lambda token: UNSPECIFIED_FLAG not in token 1029 reporting_hosts = filter(is_specified_reporting, reporting_town_names) 1030 hosts = registration_town_names + list(reporting_hosts) 1031 1032 formatted_tokens = [] 1033 for town in reporting_town_names: 1034 if UNSPECIFIED_FLAG in town: 1035 unformatted = ' '.join(filter(None, [STANDARD_FLAG, *hosts, town])) 1036 group_name = ReportingUnit._format_unspecified_group(unformatted) 1037 formatted_tokens.append(group_name) 1038 else: 1039 formatted_tokens.append(town) 1040 1041 return formatted_tokens 1042 1043 @staticmethod 1044 def _classify_fragment(fragment_name: str) -> Type[ResultGeo]: 1045 """ 1046 Return correct ResultGeo child class for a reporting towns string fragment. 1047 """ 1048 if is_unnamed_township(fragment_name): 1049 return UnnamedTownship 1050 elif UNSPECIFIED_FLAG in fragment_name: 1051 return UnspecifiedGroup 1052 else: 1053 return NamedTownship
255@dataclass 256class ResultString: 257 """ 258 A string containing one or more geographies from raw election results. 259 260 This dataclass performs initial normalization and parsing operations on a raw 261 election result string. It owns operations that are fully agnostic to which 262 geographies the tokens in the string represent. 263 264 These operations normalize variations in meaningful formatting (including 265 whitespace, delimiters, and other punctuation) so that two types of parsing can occur: 266 267 1. The splitting of result strings into a list of tokens, each 268 one representing a single geography. 269 2. The separation of tokens representing reporting geographies from tokens 270 representing non-reporting registration towns. 271 272 Args: 273 raw_string: The raw string representation of the reporting unit 274 """ 275 raw_string: str 276 277 @cached_property 278 def exists(self) -> bool: 279 if self.raw_string is None: 280 return False 281 elif len(self.raw_string) > 5: 282 return True 283 elif re.search(r'\w', self.raw_string): 284 return True 285 else: 286 return False 287 288 @cached_property 289 def normalized_string(self) -> str: 290 """ 291 Result string with capitalization, whitespace and delimiters normalized. 292 293 Errors entered in `mainegeo.lookups.Overrides.known_typos` and 294 `mainegeo.lookups.Overrides.ambiguous_groups` are also repaired. 295 296 Example: 297 >>> result = ResultString('FORT KENT/BIG TWENTY TWP/ T15 R15 WELS') 298 >>> result.normalized_string 299 'FORT KENT, BIG TWENTY TWP, T15 R15 WELS' 300 301 >>> result = ResultString('T12/R13 WELS/T9 R8 WELS') 302 >>> result.normalized_string 303 'T12 R13 WELS, T9 R8 WELS' 304 305 >>> result = ResultString('T10 SD TWP (CHERRYFIELD, FRANKLIN & MILBRIDGE)') 306 >>> result.normalized_string 307 'T10 SD TWP (CHERRYFIELD, FRANKLIN, MILBRIDGE)' 308 """ 309 initial_cleanup = [ 310 str.upper 311 , ResultString._fix_known_typos 312 , ResultString._rename_ambiguous_groups 313 , ResultString._drop_non_meaningful_chars 314 , clean_codes 315 , ResultString._normalize_delimiters 316 , normalize_whitespace 317 ] 318 if self.exists: 319 return chain_operations(self.raw_string, initial_cleanup) 320 321 @cached_property 322 def registration_town_names(self) -> List[str]: 323 """ 324 List of registration town names extracted from result string. 325 326 Note: 327 Splits strings by package-level `STANDARD_DELIMITER`. 328 329 Example: 330 >>> result = ResultString('MOUNT CHASE -- T5 R7 TWP') 331 >>> result.registration_town_names 332 ['MOUNT CHASE'] 333 334 >>> result = ResultString('T7 SD TWP (STEUBEN)') 335 >>> result.registration_town_names 336 ['STEUBEN'] 337 338 >>> result = ResultString('CROSS LAKE TWP (T17 R5)') 339 >>> result.registration_town_names 340 [] 341 342 >>> result = ResultString('ARGYLE TWP (ALTON, EDINBURG)') 343 >>> result.registration_town_names 344 ['ALTON', 'EDINBURG'] 345 """ 346 if self._registration_town_substring is None: 347 return [] 348 else: 349 reg_towns = self._registration_town_substring.split(STANDARD_DELIMITER) 350 return list(map(clean_town, reg_towns)) 351 352 @cached_property 353 def reporting_town_names(self) -> List[str]: 354 """ 355 Extract list of reporting town names from result string. 356 357 Note: 358 Drops parentheses around township alias, but does not remove alias. 359 360 Args: 361 result_str: Delimited result string with one or more towns or townships 362 363 Returns: 364 List: Reporting towns with formatting identifiers stripped 365 366 Example: 367 >>> ResultString('MOUNT CHASE--T5 R7 TWP').reporting_town_names 368 ['T5 R7'] 369 370 >>> ResultString('HERSEYTOWN, SOLDIERTOWN TWPS (MEDWAY)').reporting_town_names 371 ['HERSEYTOWN', 'SOLDIERTOWN TWPS'] 372 373 >>> ResultString('ARGYLE TWP (ALTON, EDINBURG)').reporting_town_names 374 ['ARGYLE TWP'] 375 376 >>> ResultString('BARNARD TWP, EBEEMEE TWP (T5 R9 NWP), T4 R9 NWP TWP').reporting_town_names 377 ['BARNARD TWP', 'EBEEMEE TWP T5 R9 NWP', 'T4 R9 NWP'] 378 379 >>> ResultString('BERRY/CATHANCE/MARIONTWPS (EAST MACHIAS)').reporting_town_names 380 ['BERRY', 'CATHANCE', 'MARION TWPS'] 381 """ 382 if self._registration_town_substring is None: 383 reporting_substr = self.normalized_string 384 else: 385 reporting_substr = re.sub(self._registration_town_substring, '', self.normalized_string) 386 387 reporting = reporting_substr.split(STANDARD_DELIMITER) 388 return list(map(clean_town, reporting)) 389 390 @cached_property 391 def _registration_town_substring(self) -> str: 392 """ 393 Registration town substring extracted based on SoS formatting identifiers. 394 395 SoS formatting identifiers include parentheticals and double dashes (--). 396 Parentheticals that are township aliases are ignored. 397 398 Note: 399 Substring includes formatting indicators. 400 401 Raises: 402 ValueError: If result string contains multiple registration town substrings. 403 404 Examples: 405 >>> result = ResultString('MOUNT CHASE -- T5 R7 TWP') 406 >>> result._registration_town_substring 407 'MOUNT CHASE--' 408 409 >>> result = ResultString('T7 SD TWP (STEUBEN)') 410 >>> result._registration_town_substring 411 '(STEUBEN)' 412 413 >>> result = ResultString('ARGYLE TWP (ALTON, EDINBURG)') 414 >>> result._registration_town_substring 415 '(ALTON, EDINBURG)' 416 """ 417 if self.exists: 418 flagged = REGISTRATION_PATTERN.findall(self.normalized_string) 419 substrings = list(filterfalse(is_unnamed_township, flagged)) 420 421 if len(substrings) > 1: 422 raise ValueError(f'Multiple registration town substrings found in result string: {self.normalized_string}') 423 elif len(substrings) == 0: 424 return None 425 else: 426 return substrings[0] 427 428 @staticmethod 429 def _fix_known_typos(result_str: str) -> str: 430 """ 431 Fix known typos in-place in string. 432 433 These typos are true misspellings, which are unique and unlikely to be repeated. 434 They are stored in the KNOWN_TYPOS dict in the patterns module. 435 436 Args: 437 result_str: Delimited result string with one or more towns or townships 438 """ 439 return replace_all(KNOWN_TYPOS, result_str) 440 441 @staticmethod 442 def _rename_ambiguous_groups(result_str: str) -> str: 443 """ 444 Fix ambiguous unspecified group names in-place in string. 445 446 These typos recur from time to time, but follow a general pattern. It simplifies 447 unspecified group name detection significantly if they are corrected early in 448 processing. They are stored in `lookups.Overrides.ambiguous_groups`. 449 450 Args: 451 result_str: Delimited result string with one or more towns or townships 452 453 Example: 454 >>> ResultString._rename_ambiguous_groups('MILLINOCKET -- PISCATAQUIS TWP') 455 'MILLINOCKET -- PISCATAQUIS TWPS' 456 457 >>> ResultString._rename_ambiguous_groups('PENOBSCOT TWPS') 458 'MILLINOCKET PENOBSCOT TWPS' 459 """ 460 return replace_all(AMBIGUOUS_GROUPS, result_str) 461 462 @staticmethod 463 def _drop_non_meaningful_chars(result_str: str) -> str: 464 """ 465 Drop or replace special characters that don't communicate meaningful info. 466 467 Non-meaningful characters include all punctuation characters not found in 468 `mainegeo.patterns.MEANINGFUL_CHARACTERS`. 469 470 Args: 471 result_str: Delimited result string with one or more towns or townships 472 """ 473 result = VALID_AMPERSANDS_PATTERN.sub('AND', normalize_whitespace(result_str)) 474 return DROP_CHARACTERS_PATTERN.sub('', result) 475 476 @staticmethod 477 def _normalize_delimiters(result_str: str) -> str: 478 """ 479 Replace all delimiters in result string with standard delimiter. 480 481 Args: 482 result_str: Delimited result string with one or more towns or townships 483 """ 484 delimit = NONSTANDARD_DELIMITER_PATTERN.sub(STANDARD_DELIMITER, result_str) 485 cleanup = ORPHAN_PARENTHESIS_PATTERN.sub(r'\g<result>', delimit) 486 return cleanup
A string containing one or more geographies from raw election results.
This dataclass performs initial normalization and parsing operations on a raw election result string. It owns operations that are fully agnostic to which geographies the tokens in the string represent.
These operations normalize variations in meaningful formatting (including whitespace, delimiters, and other punctuation) so that two types of parsing can occur:
- The splitting of result strings into a list of tokens, each one representing a single geography.
- The separation of tokens representing reporting geographies from tokens representing non-reporting registration towns.
Arguments:
- raw_string: The raw string representation of the reporting unit
288 @cached_property 289 def normalized_string(self) -> str: 290 """ 291 Result string with capitalization, whitespace and delimiters normalized. 292 293 Errors entered in `mainegeo.lookups.Overrides.known_typos` and 294 `mainegeo.lookups.Overrides.ambiguous_groups` are also repaired. 295 296 Example: 297 >>> result = ResultString('FORT KENT/BIG TWENTY TWP/ T15 R15 WELS') 298 >>> result.normalized_string 299 'FORT KENT, BIG TWENTY TWP, T15 R15 WELS' 300 301 >>> result = ResultString('T12/R13 WELS/T9 R8 WELS') 302 >>> result.normalized_string 303 'T12 R13 WELS, T9 R8 WELS' 304 305 >>> result = ResultString('T10 SD TWP (CHERRYFIELD, FRANKLIN & MILBRIDGE)') 306 >>> result.normalized_string 307 'T10 SD TWP (CHERRYFIELD, FRANKLIN, MILBRIDGE)' 308 """ 309 initial_cleanup = [ 310 str.upper 311 , ResultString._fix_known_typos 312 , ResultString._rename_ambiguous_groups 313 , ResultString._drop_non_meaningful_chars 314 , clean_codes 315 , ResultString._normalize_delimiters 316 , normalize_whitespace 317 ] 318 if self.exists: 319 return chain_operations(self.raw_string, initial_cleanup)
Result string with capitalization, whitespace and delimiters normalized.
Errors entered in mainegeo.lookups.Overrides.known_typos and
mainegeo.lookups.Overrides.ambiguous_groups are also repaired.
Example:
>>> result = ResultString('FORT KENT/BIG TWENTY TWP/ T15 R15 WELS') >>> result.normalized_string 'FORT KENT, BIG TWENTY TWP, T15 R15 WELS'>>> result = ResultString('T12/R13 WELS/T9 R8 WELS') >>> result.normalized_string 'T12 R13 WELS, T9 R8 WELS'>>> result = ResultString('T10 SD TWP (CHERRYFIELD, FRANKLIN & MILBRIDGE)') >>> result.normalized_string 'T10 SD TWP (CHERRYFIELD, FRANKLIN, MILBRIDGE)'
321 @cached_property 322 def registration_town_names(self) -> List[str]: 323 """ 324 List of registration town names extracted from result string. 325 326 Note: 327 Splits strings by package-level `STANDARD_DELIMITER`. 328 329 Example: 330 >>> result = ResultString('MOUNT CHASE -- T5 R7 TWP') 331 >>> result.registration_town_names 332 ['MOUNT CHASE'] 333 334 >>> result = ResultString('T7 SD TWP (STEUBEN)') 335 >>> result.registration_town_names 336 ['STEUBEN'] 337 338 >>> result = ResultString('CROSS LAKE TWP (T17 R5)') 339 >>> result.registration_town_names 340 [] 341 342 >>> result = ResultString('ARGYLE TWP (ALTON, EDINBURG)') 343 >>> result.registration_town_names 344 ['ALTON', 'EDINBURG'] 345 """ 346 if self._registration_town_substring is None: 347 return [] 348 else: 349 reg_towns = self._registration_town_substring.split(STANDARD_DELIMITER) 350 return list(map(clean_town, reg_towns))
List of registration town names extracted from result string.
Note:
Splits strings by package-level
STANDARD_DELIMITER.
Example:
>>> result = ResultString('MOUNT CHASE -- T5 R7 TWP') >>> result.registration_town_names ['MOUNT CHASE']>>> result = ResultString('T7 SD TWP (STEUBEN)') >>> result.registration_town_names ['STEUBEN']>>> result = ResultString('CROSS LAKE TWP (T17 R5)') >>> result.registration_town_names []>>> result = ResultString('ARGYLE TWP (ALTON, EDINBURG)') >>> result.registration_town_names ['ALTON', 'EDINBURG']
352 @cached_property 353 def reporting_town_names(self) -> List[str]: 354 """ 355 Extract list of reporting town names from result string. 356 357 Note: 358 Drops parentheses around township alias, but does not remove alias. 359 360 Args: 361 result_str: Delimited result string with one or more towns or townships 362 363 Returns: 364 List: Reporting towns with formatting identifiers stripped 365 366 Example: 367 >>> ResultString('MOUNT CHASE--T5 R7 TWP').reporting_town_names 368 ['T5 R7'] 369 370 >>> ResultString('HERSEYTOWN, SOLDIERTOWN TWPS (MEDWAY)').reporting_town_names 371 ['HERSEYTOWN', 'SOLDIERTOWN TWPS'] 372 373 >>> ResultString('ARGYLE TWP (ALTON, EDINBURG)').reporting_town_names 374 ['ARGYLE TWP'] 375 376 >>> ResultString('BARNARD TWP, EBEEMEE TWP (T5 R9 NWP), T4 R9 NWP TWP').reporting_town_names 377 ['BARNARD TWP', 'EBEEMEE TWP T5 R9 NWP', 'T4 R9 NWP'] 378 379 >>> ResultString('BERRY/CATHANCE/MARIONTWPS (EAST MACHIAS)').reporting_town_names 380 ['BERRY', 'CATHANCE', 'MARION TWPS'] 381 """ 382 if self._registration_town_substring is None: 383 reporting_substr = self.normalized_string 384 else: 385 reporting_substr = re.sub(self._registration_town_substring, '', self.normalized_string) 386 387 reporting = reporting_substr.split(STANDARD_DELIMITER) 388 return list(map(clean_town, reporting))
Extract list of reporting town names from result string.
Note:
Drops parentheses around township alias, but does not remove alias.
Arguments:
- result_str: Delimited result string with one or more towns or townships
Returns:
List: Reporting towns with formatting identifiers stripped
Example:
>>> ResultString('MOUNT CHASE--T5 R7 TWP').reporting_town_names ['T5 R7']>>> ResultString('HERSEYTOWN, SOLDIERTOWN TWPS (MEDWAY)').reporting_town_names ['HERSEYTOWN', 'SOLDIERTOWN TWPS']>>> ResultString('ARGYLE TWP (ALTON, EDINBURG)').reporting_town_names ['ARGYLE TWP']>>> ResultString('BARNARD TWP, EBEEMEE TWP (T5 R9 NWP), T4 R9 NWP TWP').reporting_town_names ['BARNARD TWP', 'EBEEMEE TWP T5 R9 NWP', 'T4 R9 NWP']>>> ResultString('BERRY/CATHANCE/MARIONTWPS (EAST MACHIAS)').reporting_town_names ['BERRY', 'CATHANCE', 'MARION TWPS']
618@dataclass 619class ReportingUnit: 620 """A collection of towns and unspecified groups parsed from a `ResultString`. 621 622 This class performs normalization operations on individual fragments of a 623 delimited string, coerces them into objects representing different geography 624 types, and attempts to match them to known Maine geographies. It provides several 625 options for representing the fully parsed unit as a string or dictionary. 626 627 Args: 628 result_string: A `ResultString` object 629 county: A `County` object 630 strict: True if exception should be raised when a match fails 631 """ 632 result_string: ResultString 633 county: County 634 strict: bool = False 635 636 @classmethod 637 def from_strings( 638 cls, 639 result_str: str, 640 county_code: str, 641 strict: bool = False 642 ) -> "ReportingUnit": 643 """ Factory method to create a fully processed ReportingUnit. 644 645 Examples: 646 >>> result = ReportingUnit.from_strings('PRENTISS TWP (WEBSTER PLT)', 'PEN') 647 >>> result.formatted_string 648 'Prentiss Twp T7 R3 NBPP [Webster Plt]' 649 >>> result.reporting_town_names 650 ['Prentiss Twp T7 R3 NBPP'] 651 >>> result.registration_town_names 652 ['Webster Plt'] 653 >>> result.unspecified_groups 654 [] 655 656 >>> result = ReportingUnit.from_strings('MEDWAY -- GRINDSTONE/SOLDIERTOWN TWPS', 'PEN') 657 >>> result.formatted_string 658 'Grindstone Twp [Medway], Soldiertown Twp T2 R7 WELS [Medway]' 659 >>> result.reporting_town_names 660 ['Grindstone Twp', 'Soldiertown Twp T2 R7 WELS'] 661 >>> result.registration_town_names 662 ['Medway'] 663 664 >>> result = ReportingUnit.from_strings('FRANKLIN/T9 T10 SD TWPS', 'HAN') 665 >>> result.formatted_string 666 'Franklin, T9 SD BPP, T10 SD BPP' 667 >>> result.reporting_town_names 668 ['Franklin', 'T9 SD BPP', 'T10 SD BPP'] 669 >>> result.registration_town_names 670 [] 671 672 >>> result = ReportingUnit.from_strings('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') 673 >>> result.formatted_string 674 'Millinocket, Unspecified Piscataquis County Twps [Millinocket]' 675 >>> result.reporting_town_names 676 ['Millinocket', 'Unspecified Piscataquis County Twps'] 677 >>> result.registration_town_names 678 ['Millinocket'] 679 >>> len(result.unspecified_groups) 680 1 681 682 >>> result = ReportingUnit.from_strings('MILLINOCKET/PIS TWPS/PEN TWPS', 'PEN') 683 >>> result.reporting_town_names 684 ['Millinocket', 'Unspecified Piscataquis County Twps', 'Unspecified Penobscot County Twps'] 685 >>> result.registration_town_names 686 ['Millinocket'] 687 >>> len(result.unspecified_groups) 688 2 689 690 >>> result = ReportingUnit.from_strings('MEDWAY TOWNSHIPS', 'PEN') 691 >>> result.formatted_string 692 'Unspecified Twps [Medway]' 693 >>> result.reporting_town_names 694 ['Unspecified Twps'] 695 >>> result.registration_town_names 696 ['Medway'] 697 >>> len(result.unspecified_groups) 698 1 699 """ 700 unit = cls( 701 result_string = ResultString(result_str), 702 county = County(code = county_code), 703 strict = strict 704 ) 705 return unit 706 707 @property 708 def raw_string(self) -> str: 709 """ 710 Original SoS name for this reporting unit. 711 712 Examples: 713 >>> unit = ReportingUnit.from_strings('WEBSTER PLT -- PRENTISS TWP', 'PEN') 714 >>> unit.raw_string 715 'WEBSTER PLT -- PRENTISS TWP' 716 """ 717 return self.result_string.raw_string 718 719 @cached_property 720 def formatted_string(self) -> str: 721 """ 722 A formatted string representation of this reporting unit. 723 724 Examples: 725 >>> args = ('T12/R13 & T9/R8 WELS (ASHLAND)', 'ARO') 726 >>> ReportingUnit.from_strings(*args).formatted_string 727 'T12 R13 WELS [Ashland], T9 R8 WELS [Ashland]' 728 729 >>> args = ('GRINDSTONE/HERSEYTOWN/SOLDIERTOWN TWP', 'PEN') 730 >>> ReportingUnit.from_strings(*args).formatted_string 731 'Grindstone Twp, Herseytown Twp, Soldiertown Twp T2 R7 WELS' 732 733 >>> args = ('SHERMAN (AND BENEDICTA & SILVER RIDGE TWPS) ', 'ARO') 734 >>> ReportingUnit.from_strings(*args).formatted_string 735 'Sherman, Benedicta Twp, Silver Ridge Twp' 736 737 >>> args = ('JACKMAN TWPS', 'SOM') 738 >>> ReportingUnit.from_strings(*args).formatted_string 739 'Unspecified Twps [Jackman]' 740 741 >>> args = ('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') 742 >>> ReportingUnit.from_strings(*args).formatted_string 743 'Millinocket, Unspecified Piscataquis County Twps [Millinocket]' 744 """ 745 reporting = [ 746 town + f' [{self.registration_string}]' 747 if 748 len(self.registration_town_names) > 0 749 and town not in self.registration_town_names 750 else town 751 for town in self.reporting_town_names 752 ] 753 return STANDARD_DELIMITER.join(reporting) 754 755 @cached_property 756 def reporting_string(self) -> str: 757 """ 758 A formatted string representation of reporting towns in this unit. 759 760 Examples: 761 >>> args = ('WEBSTER PLT -- PRENTISS TWP', 'PEN') 762 >>> ReportingUnit.from_strings(*args).reporting_string 763 'Prentiss Twp T7 R3 NBPP' 764 765 >>> args = ('T12/R13 & T9/R8 WELS (ASHLAND)', 'ARO') 766 >>> ReportingUnit.from_strings(*args).reporting_string 767 'T12 R13 WELS, T9 R8 WELS' 768 769 >>> args = ('JACKMAN TWPS', 'SOM') 770 >>> ReportingUnit.from_strings(*args).reporting_string 771 'Unspecified Twps' 772 773 >>> args = ('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') 774 >>> ReportingUnit.from_strings(*args).reporting_string 775 'Millinocket, Unspecified Piscataquis County Twps' 776 777 >>> args = ('MILLINOCKET/PEN TWPS/PIS TWPS', 'PEN') 778 >>> ReportingUnit.from_strings(*args).reporting_string 779 'Millinocket, Unspecified Penobscot County Twps, Unspecified Piscataquis County Twps' 780 """ 781 return STANDARD_DELIMITER.join(self.reporting_town_names) 782 783 @cached_property 784 def registration_string(self) -> str: 785 """ 786 A formatted string representation of registration towns in this unit. 787 788 Examples: 789 >>> args = ('WEBSTER PLT -- PRENTISS TWP', 'PEN') 790 >>> ReportingUnit.from_strings(*args).registration_string 791 'Webster Plt' 792 793 >>> args = ('WYMAN TWP (CARRABASSETT VALLEY & EUSTIS)', 'FRA') 794 >>> ReportingUnit.from_strings(*args).registration_string 795 'Carrabassett Valley, Eustis' 796 797 >>> args = ('JACKMAN TWPS', 'SOM') 798 >>> ReportingUnit.from_strings(*args).registration_string 799 'Jackman' 800 801 >>> args = ('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') 802 >>> ReportingUnit.from_strings(*args).registration_string 803 'Millinocket' 804 805 >>> args = ('MILLINOCKET/PIS TWPS/PEN TWPS', 'PEN') 806 >>> ReportingUnit.from_strings(*args).registration_string 807 'Millinocket' 808 """ 809 return STANDARD_DELIMITER.join(self.registration_town_names) 810 811 @cached_property 812 def reporting_town_names(self) -> list[str]: 813 """ 814 List of reporting town names. Canonical name if match was found, else raw name. 815 """ 816 return [town.consensus_name for town in self.reporting_towns] 817 818 @cached_property 819 def registration_town_names(self) -> list[str]: 820 """ 821 List of registration town names. Canonical name if match was found, else raw name. 822 """ 823 return [town.consensus_name for town in self.registration_towns] 824 825 @cached_property 826 def registration_towns(self) -> List[NamedTownship]: 827 """ 828 List of registration towns as `NamedTownship` objects. 829 """ 830 towns = set() 831 for name in self.result_string.registration_town_names: 832 regtown = NamedTownship(name, self.county, strict = self.strict) 833 towns.add(regtown) 834 835 for group in self.unspecified_groups: 836 towns.add(group.group_registration_town) 837 838 return sorted(towns, key=lambda town: town.consensus_name) 839 840 @cached_property 841 def reporting_towns(self) -> List[ResultGeo]: 842 """ 843 List of reporting towns, townships and groups as `ResultGeo` child objects. 844 """ 845 towns = [] 846 847 if self.result_string.exists: 848 formatted_reporting_names = ReportingUnit._format_reporting_towns( 849 self.result_string.reporting_town_names, 850 self.result_string.registration_town_names, 851 self.has_unspecified_group 852 ) 853 for name in formatted_reporting_names: 854 ResultClass = ReportingUnit._classify_fragment(name) 855 reporting_town = ResultClass(name, self.county, strict = self.strict) 856 towns.append(reporting_town) 857 858 return towns 859 860 @cached_property 861 def specified_reporting_towns(self) -> List[Municipality]: 862 """ 863 List of reporting units that are not unspecified groups. 864 865 Examples: 866 >>> unit = ReportingUnit.from_strings('WYMAN TWP/SPRING LAKE TWP', 'FRA') 867 >>> [town.name for town in unit.specified_reporting_towns] 868 ['WYMAN TWP', 'SPRING LAKE TWP'] 869 870 >>> unit = ReportingUnit.from_strings('WYMAN TWP (CARRABASSETT VALLEY)', 'FRA') 871 >>> [town.name for town in unit.specified_reporting_towns] 872 ['WYMAN TWP'] 873 874 >>> unit = ReportingUnit.from_strings('MILLINOCKET/TWPS', 'PEN') 875 >>> [town.name for town in unit.specified_reporting_towns] 876 ['MILLINOCKET'] 877 878 >>> unit = ReportingUnit.from_strings('MILLINOCKET TWPS', 'PEN') 879 >>> [town.name for town in unit.specified_reporting_towns] 880 [] 881 """ 882 return [t for t in self.reporting_towns if type(t) != UnspecifiedGroup] 883 884 @cached_property 885 def unspecified_groups(self) -> List[ResultGeo]: 886 """ 887 List of reporting units that are unspecified groups. 888 889 Examples: 890 >>> unit = ReportingUnit.from_strings('MILLINOCKET/PEN TWPS', 'PEN') 891 >>> groups = unit.unspecified_groups 892 >>> [group.name for group in groups] 893 ['UNSPECIFIED MILLINOCKET TWPS [PEN]'] 894 >>> [group.canonical_name for group in groups] 895 ['Unspecified Penobscot County Twps'] 896 >>> [group.group_registration_town.canonical_name for group in groups] 897 ['Millinocket'] 898 """ 899 return [t for t in self.reporting_towns if type(t) == UnspecifiedGroup] 900 901 @cached_property 902 def has_unspecified_group(self) -> bool: 903 """ 904 Return True if the result object includes an unspecified group, else False. 905 906 Examples: 907 >>> unit = ReportingUnit.from_strings('MEDWAY/TOWNSHIPS', 'PEN') 908 >>> unit.has_unspecified_group 909 True 910 911 >>> unit = ReportingUnit.from_strings('ADAMSTOWN/LOWER CUPSUPTIC TWPS (RANGELEY)', 'OXF') 912 >>> unit.has_unspecified_group 913 False 914 915 >>> unit = ReportingUnit.from_strings('MILLINOCKET PISCATAQUIS TWPS', 'PIS') 916 >>> unit.has_unspecified_group 917 True 918 919 >>> unit = ReportingUnit.from_strings('MILLINOCKET/PEN TWPS', 'PEN') 920 >>> unit.has_unspecified_group 921 True 922 923 >>> unit = ReportingUnit.from_strings('LEXINGTON & SPRING LAKE TWPS', 'SOM') 924 >>> unit.has_unspecified_group 925 False 926 """ 927 reporting = self.result_string.reporting_town_names 928 registration = self.result_string.registration_town_names 929 group_name = ' '.join(filter(None, [*registration, *reporting])) 930 931 if len(registration) > 1: 932 return False 933 elif len(reporting) in (1, 2) and UNSPECIFIED_FLAG in reporting: 934 return True 935 elif all(UNSPECIFIED_FLAG in town for town in reporting): 936 return True 937 elif MULTI_COUNTY_PATTERN.match(group_name): 938 return True 939 else: 940 return False 941 942 @cached_property 943 def is_matched(self) -> bool: 944 """True if every named geography in this unit matched a known town.""" 945 if not self.result_string.exists: 946 return False 947 else: 948 tokens = [*self.reporting_towns, *self.registration_towns] 949 return bool(tokens) and all(t.is_matched for t in tokens) 950 951 def to_dict(self) -> dict[str]: 952 return { 953 'raw_str': self.raw_string, 954 'formatted_str': self.formatted_string, 955 'reporting_str': self.reporting_string, 956 'registration_str': self.registration_string, 957 'reporting': { 958 'specified': [town.to_dict() for town in self.specified_reporting_towns], 959 'unspecified': [group.to_dict() for group in self.unspecified_groups] 960 }, 961 'registration': [town.to_dict() for town in self.registration_towns], 962 'county': asdict(self.county), 963 'is_matched': self.is_matched 964 } 965 966 @staticmethod 967 def _format_reporting_towns( 968 reporting_town_names: List[str], 969 registration_town_names: List[str], 970 has_unspecified_group: bool) -> List[str]: 971 """ 972 Apply consistent format to towns and unspecified groups. 973 974 Args: 975 reporting_towns: List of one or more towns 976 registration_towns: List of non-reporting registration towns (if any) 977 has_unspecified_group: True if unit contains unspecified group, else False 978 979 Returns: 980 list: Reporting towns with standard formatting applied. 981 982 Examples: 983 >>> ReportingUnit._format_reporting_towns(['LEXINGTON', 'SPRING LAKE TWPS'], [], False) 984 ['LEXINGTON', 'SPRING LAKE TWP'] 985 986 >>> ReportingUnit._format_reporting_towns(['FRANKLIN', 'TWPS'], [], True) 987 ['FRANKLIN', 'UNSPECIFIED FRANKLIN TWPS'] 988 989 >>> ReportingUnit._format_reporting_towns(['PENOBSCOT TWPS'], ['MILLINOCKET'], True) 990 ['UNSPECIFIED MILLINOCKET TWPS [PEN]'] 991 """ 992 reporting = [ 993 ReportingUnit._format_plural(town, has_unspecified_group) 994 for town in reporting_town_names 995 ] 996 997 if has_unspecified_group: 998 return ReportingUnit._name_unspecified_group(reporting, registration_town_names) 999 else: 1000 return reporting 1001 1002 @staticmethod 1003 def _format_plural(town: str, has_unspecified_group: bool) -> str: 1004 """ 1005 Correct errors of pluralization in town or group names. 1006 1007 After this function is used, the presence or absence of a plural in a town name 1008 reliably indicates whether it is an unspecified group. 1009 """ 1010 if has_unspecified_group: 1011 return SINGULAR_PATTERN.sub(PLURAL, town) 1012 else: 1013 return PLURAL_PATTERN.sub(SINGULAR, town) 1014 1015 @staticmethod 1016 def _format_unspecified_group(group_name: str) -> str: 1017 """ 1018 Apply special format to unspecified groups that include a county. 1019 """ 1020 return MULTI_COUNTY_PATTERN.sub(MULTI_COUNTY_FORMAT, group_name) 1021 1022 @staticmethod 1023 def _name_unspecified_group( 1024 reporting_town_names: List[str], 1025 registration_town_names: List[str]) -> List[str]: 1026 """ 1027 Label unspecified groups with their reporting town and a standard 'unspecified' flag. 1028 """ 1029 is_specified_reporting = lambda token: UNSPECIFIED_FLAG not in token 1030 reporting_hosts = filter(is_specified_reporting, reporting_town_names) 1031 hosts = registration_town_names + list(reporting_hosts) 1032 1033 formatted_tokens = [] 1034 for town in reporting_town_names: 1035 if UNSPECIFIED_FLAG in town: 1036 unformatted = ' '.join(filter(None, [STANDARD_FLAG, *hosts, town])) 1037 group_name = ReportingUnit._format_unspecified_group(unformatted) 1038 formatted_tokens.append(group_name) 1039 else: 1040 formatted_tokens.append(town) 1041 1042 return formatted_tokens 1043 1044 @staticmethod 1045 def _classify_fragment(fragment_name: str) -> Type[ResultGeo]: 1046 """ 1047 Return correct ResultGeo child class for a reporting towns string fragment. 1048 """ 1049 if is_unnamed_township(fragment_name): 1050 return UnnamedTownship 1051 elif UNSPECIFIED_FLAG in fragment_name: 1052 return UnspecifiedGroup 1053 else: 1054 return NamedTownship
A collection of towns and unspecified groups parsed from a ResultString.
This class performs normalization operations on individual fragments of a delimited string, coerces them into objects representing different geography types, and attempts to match them to known Maine geographies. It provides several options for representing the fully parsed unit as a string or dictionary.
Arguments:
- result_string: A
ResultStringobject - county: A
Countyobject - strict: True if exception should be raised when a match fails
636 @classmethod 637 def from_strings( 638 cls, 639 result_str: str, 640 county_code: str, 641 strict: bool = False 642 ) -> "ReportingUnit": 643 """ Factory method to create a fully processed ReportingUnit. 644 645 Examples: 646 >>> result = ReportingUnit.from_strings('PRENTISS TWP (WEBSTER PLT)', 'PEN') 647 >>> result.formatted_string 648 'Prentiss Twp T7 R3 NBPP [Webster Plt]' 649 >>> result.reporting_town_names 650 ['Prentiss Twp T7 R3 NBPP'] 651 >>> result.registration_town_names 652 ['Webster Plt'] 653 >>> result.unspecified_groups 654 [] 655 656 >>> result = ReportingUnit.from_strings('MEDWAY -- GRINDSTONE/SOLDIERTOWN TWPS', 'PEN') 657 >>> result.formatted_string 658 'Grindstone Twp [Medway], Soldiertown Twp T2 R7 WELS [Medway]' 659 >>> result.reporting_town_names 660 ['Grindstone Twp', 'Soldiertown Twp T2 R7 WELS'] 661 >>> result.registration_town_names 662 ['Medway'] 663 664 >>> result = ReportingUnit.from_strings('FRANKLIN/T9 T10 SD TWPS', 'HAN') 665 >>> result.formatted_string 666 'Franklin, T9 SD BPP, T10 SD BPP' 667 >>> result.reporting_town_names 668 ['Franklin', 'T9 SD BPP', 'T10 SD BPP'] 669 >>> result.registration_town_names 670 [] 671 672 >>> result = ReportingUnit.from_strings('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') 673 >>> result.formatted_string 674 'Millinocket, Unspecified Piscataquis County Twps [Millinocket]' 675 >>> result.reporting_town_names 676 ['Millinocket', 'Unspecified Piscataquis County Twps'] 677 >>> result.registration_town_names 678 ['Millinocket'] 679 >>> len(result.unspecified_groups) 680 1 681 682 >>> result = ReportingUnit.from_strings('MILLINOCKET/PIS TWPS/PEN TWPS', 'PEN') 683 >>> result.reporting_town_names 684 ['Millinocket', 'Unspecified Piscataquis County Twps', 'Unspecified Penobscot County Twps'] 685 >>> result.registration_town_names 686 ['Millinocket'] 687 >>> len(result.unspecified_groups) 688 2 689 690 >>> result = ReportingUnit.from_strings('MEDWAY TOWNSHIPS', 'PEN') 691 >>> result.formatted_string 692 'Unspecified Twps [Medway]' 693 >>> result.reporting_town_names 694 ['Unspecified Twps'] 695 >>> result.registration_town_names 696 ['Medway'] 697 >>> len(result.unspecified_groups) 698 1 699 """ 700 unit = cls( 701 result_string = ResultString(result_str), 702 county = County(code = county_code), 703 strict = strict 704 ) 705 return unit
Factory method to create a fully processed ReportingUnit.
Examples:
>>> result = ReportingUnit.from_strings('PRENTISS TWP (WEBSTER PLT)', 'PEN') >>> result.formatted_string 'Prentiss Twp T7 R3 NBPP [Webster Plt]' >>> result.reporting_town_names ['Prentiss Twp T7 R3 NBPP'] >>> result.registration_town_names ['Webster Plt'] >>> result.unspecified_groups []>>> result = ReportingUnit.from_strings('MEDWAY -- GRINDSTONE/SOLDIERTOWN TWPS', 'PEN') >>> result.formatted_string 'Grindstone Twp [Medway], Soldiertown Twp T2 R7 WELS [Medway]' >>> result.reporting_town_names ['Grindstone Twp', 'Soldiertown Twp T2 R7 WELS'] >>> result.registration_town_names ['Medway']>>> result = ReportingUnit.from_strings('FRANKLIN/T9 T10 SD TWPS', 'HAN') >>> result.formatted_string 'Franklin, T9 SD BPP, T10 SD BPP' >>> result.reporting_town_names ['Franklin', 'T9 SD BPP', 'T10 SD BPP'] >>> result.registration_town_names []>>> result = ReportingUnit.from_strings('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') >>> result.formatted_string 'Millinocket, Unspecified Piscataquis County Twps [Millinocket]' >>> result.reporting_town_names ['Millinocket', 'Unspecified Piscataquis County Twps'] >>> result.registration_town_names ['Millinocket'] >>> len(result.unspecified_groups) 1>>> result = ReportingUnit.from_strings('MILLINOCKET/PIS TWPS/PEN TWPS', 'PEN') >>> result.reporting_town_names ['Millinocket', 'Unspecified Piscataquis County Twps', 'Unspecified Penobscot County Twps'] >>> result.registration_town_names ['Millinocket'] >>> len(result.unspecified_groups) 2>>> result = ReportingUnit.from_strings('MEDWAY TOWNSHIPS', 'PEN') >>> result.formatted_string 'Unspecified Twps [Medway]' >>> result.reporting_town_names ['Unspecified Twps'] >>> result.registration_town_names ['Medway'] >>> len(result.unspecified_groups) 1
707 @property 708 def raw_string(self) -> str: 709 """ 710 Original SoS name for this reporting unit. 711 712 Examples: 713 >>> unit = ReportingUnit.from_strings('WEBSTER PLT -- PRENTISS TWP', 'PEN') 714 >>> unit.raw_string 715 'WEBSTER PLT -- PRENTISS TWP' 716 """ 717 return self.result_string.raw_string
Original SoS name for this reporting unit.
Examples:
>>> unit = ReportingUnit.from_strings('WEBSTER PLT -- PRENTISS TWP', 'PEN') >>> unit.raw_string 'WEBSTER PLT -- PRENTISS TWP'
719 @cached_property 720 def formatted_string(self) -> str: 721 """ 722 A formatted string representation of this reporting unit. 723 724 Examples: 725 >>> args = ('T12/R13 & T9/R8 WELS (ASHLAND)', 'ARO') 726 >>> ReportingUnit.from_strings(*args).formatted_string 727 'T12 R13 WELS [Ashland], T9 R8 WELS [Ashland]' 728 729 >>> args = ('GRINDSTONE/HERSEYTOWN/SOLDIERTOWN TWP', 'PEN') 730 >>> ReportingUnit.from_strings(*args).formatted_string 731 'Grindstone Twp, Herseytown Twp, Soldiertown Twp T2 R7 WELS' 732 733 >>> args = ('SHERMAN (AND BENEDICTA & SILVER RIDGE TWPS) ', 'ARO') 734 >>> ReportingUnit.from_strings(*args).formatted_string 735 'Sherman, Benedicta Twp, Silver Ridge Twp' 736 737 >>> args = ('JACKMAN TWPS', 'SOM') 738 >>> ReportingUnit.from_strings(*args).formatted_string 739 'Unspecified Twps [Jackman]' 740 741 >>> args = ('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') 742 >>> ReportingUnit.from_strings(*args).formatted_string 743 'Millinocket, Unspecified Piscataquis County Twps [Millinocket]' 744 """ 745 reporting = [ 746 town + f' [{self.registration_string}]' 747 if 748 len(self.registration_town_names) > 0 749 and town not in self.registration_town_names 750 else town 751 for town in self.reporting_town_names 752 ] 753 return STANDARD_DELIMITER.join(reporting)
A formatted string representation of this reporting unit.
Examples:
>>> args = ('T12/R13 & T9/R8 WELS (ASHLAND)', 'ARO') >>> ReportingUnit.from_strings(*args).formatted_string 'T12 R13 WELS [Ashland], T9 R8 WELS [Ashland]'>>> args = ('GRINDSTONE/HERSEYTOWN/SOLDIERTOWN TWP', 'PEN') >>> ReportingUnit.from_strings(*args).formatted_string 'Grindstone Twp, Herseytown Twp, Soldiertown Twp T2 R7 WELS'>>> args = ('SHERMAN (AND BENEDICTA & SILVER RIDGE TWPS) ', 'ARO') >>> ReportingUnit.from_strings(*args).formatted_string 'Sherman, Benedicta Twp, Silver Ridge Twp'>>> args = ('JACKMAN TWPS', 'SOM') >>> ReportingUnit.from_strings(*args).formatted_string 'Unspecified Twps [Jackman]'>>> args = ('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') >>> ReportingUnit.from_strings(*args).formatted_string 'Millinocket, Unspecified Piscataquis County Twps [Millinocket]'
755 @cached_property 756 def reporting_string(self) -> str: 757 """ 758 A formatted string representation of reporting towns in this unit. 759 760 Examples: 761 >>> args = ('WEBSTER PLT -- PRENTISS TWP', 'PEN') 762 >>> ReportingUnit.from_strings(*args).reporting_string 763 'Prentiss Twp T7 R3 NBPP' 764 765 >>> args = ('T12/R13 & T9/R8 WELS (ASHLAND)', 'ARO') 766 >>> ReportingUnit.from_strings(*args).reporting_string 767 'T12 R13 WELS, T9 R8 WELS' 768 769 >>> args = ('JACKMAN TWPS', 'SOM') 770 >>> ReportingUnit.from_strings(*args).reporting_string 771 'Unspecified Twps' 772 773 >>> args = ('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') 774 >>> ReportingUnit.from_strings(*args).reporting_string 775 'Millinocket, Unspecified Piscataquis County Twps' 776 777 >>> args = ('MILLINOCKET/PEN TWPS/PIS TWPS', 'PEN') 778 >>> ReportingUnit.from_strings(*args).reporting_string 779 'Millinocket, Unspecified Penobscot County Twps, Unspecified Piscataquis County Twps' 780 """ 781 return STANDARD_DELIMITER.join(self.reporting_town_names)
A formatted string representation of reporting towns in this unit.
Examples:
>>> args = ('WEBSTER PLT -- PRENTISS TWP', 'PEN') >>> ReportingUnit.from_strings(*args).reporting_string 'Prentiss Twp T7 R3 NBPP'>>> args = ('T12/R13 & T9/R8 WELS (ASHLAND)', 'ARO') >>> ReportingUnit.from_strings(*args).reporting_string 'T12 R13 WELS, T9 R8 WELS'>>> args = ('JACKMAN TWPS', 'SOM') >>> ReportingUnit.from_strings(*args).reporting_string 'Unspecified Twps'>>> args = ('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') >>> ReportingUnit.from_strings(*args).reporting_string 'Millinocket, Unspecified Piscataquis County Twps'>>> args = ('MILLINOCKET/PEN TWPS/PIS TWPS', 'PEN') >>> ReportingUnit.from_strings(*args).reporting_string 'Millinocket, Unspecified Penobscot County Twps, Unspecified Piscataquis County Twps'
783 @cached_property 784 def registration_string(self) -> str: 785 """ 786 A formatted string representation of registration towns in this unit. 787 788 Examples: 789 >>> args = ('WEBSTER PLT -- PRENTISS TWP', 'PEN') 790 >>> ReportingUnit.from_strings(*args).registration_string 791 'Webster Plt' 792 793 >>> args = ('WYMAN TWP (CARRABASSETT VALLEY & EUSTIS)', 'FRA') 794 >>> ReportingUnit.from_strings(*args).registration_string 795 'Carrabassett Valley, Eustis' 796 797 >>> args = ('JACKMAN TWPS', 'SOM') 798 >>> ReportingUnit.from_strings(*args).registration_string 799 'Jackman' 800 801 >>> args = ('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') 802 >>> ReportingUnit.from_strings(*args).registration_string 803 'Millinocket' 804 805 >>> args = ('MILLINOCKET/PIS TWPS/PEN TWPS', 'PEN') 806 >>> ReportingUnit.from_strings(*args).registration_string 807 'Millinocket' 808 """ 809 return STANDARD_DELIMITER.join(self.registration_town_names)
A formatted string representation of registration towns in this unit.
Examples:
>>> args = ('WEBSTER PLT -- PRENTISS TWP', 'PEN') >>> ReportingUnit.from_strings(*args).registration_string 'Webster Plt'>>> args = ('WYMAN TWP (CARRABASSETT VALLEY & EUSTIS)', 'FRA') >>> ReportingUnit.from_strings(*args).registration_string 'Carrabassett Valley, Eustis'>>> args = ('JACKMAN TWPS', 'SOM') >>> ReportingUnit.from_strings(*args).registration_string 'Jackman'>>> args = ('MILLINOCKET/PISCATAQUIS TWPS', 'PEN') >>> ReportingUnit.from_strings(*args).registration_string 'Millinocket'>>> args = ('MILLINOCKET/PIS TWPS/PEN TWPS', 'PEN') >>> ReportingUnit.from_strings(*args).registration_string 'Millinocket'
811 @cached_property 812 def reporting_town_names(self) -> list[str]: 813 """ 814 List of reporting town names. Canonical name if match was found, else raw name. 815 """ 816 return [town.consensus_name for town in self.reporting_towns]
List of reporting town names. Canonical name if match was found, else raw name.
818 @cached_property 819 def registration_town_names(self) -> list[str]: 820 """ 821 List of registration town names. Canonical name if match was found, else raw name. 822 """ 823 return [town.consensus_name for town in self.registration_towns]
List of registration town names. Canonical name if match was found, else raw name.
825 @cached_property 826 def registration_towns(self) -> List[NamedTownship]: 827 """ 828 List of registration towns as `NamedTownship` objects. 829 """ 830 towns = set() 831 for name in self.result_string.registration_town_names: 832 regtown = NamedTownship(name, self.county, strict = self.strict) 833 towns.add(regtown) 834 835 for group in self.unspecified_groups: 836 towns.add(group.group_registration_town) 837 838 return sorted(towns, key=lambda town: town.consensus_name)
List of registration towns as NamedTownship objects.
840 @cached_property 841 def reporting_towns(self) -> List[ResultGeo]: 842 """ 843 List of reporting towns, townships and groups as `ResultGeo` child objects. 844 """ 845 towns = [] 846 847 if self.result_string.exists: 848 formatted_reporting_names = ReportingUnit._format_reporting_towns( 849 self.result_string.reporting_town_names, 850 self.result_string.registration_town_names, 851 self.has_unspecified_group 852 ) 853 for name in formatted_reporting_names: 854 ResultClass = ReportingUnit._classify_fragment(name) 855 reporting_town = ResultClass(name, self.county, strict = self.strict) 856 towns.append(reporting_town) 857 858 return towns
List of reporting towns, townships and groups as ResultGeo child objects.
860 @cached_property 861 def specified_reporting_towns(self) -> List[Municipality]: 862 """ 863 List of reporting units that are not unspecified groups. 864 865 Examples: 866 >>> unit = ReportingUnit.from_strings('WYMAN TWP/SPRING LAKE TWP', 'FRA') 867 >>> [town.name for town in unit.specified_reporting_towns] 868 ['WYMAN TWP', 'SPRING LAKE TWP'] 869 870 >>> unit = ReportingUnit.from_strings('WYMAN TWP (CARRABASSETT VALLEY)', 'FRA') 871 >>> [town.name for town in unit.specified_reporting_towns] 872 ['WYMAN TWP'] 873 874 >>> unit = ReportingUnit.from_strings('MILLINOCKET/TWPS', 'PEN') 875 >>> [town.name for town in unit.specified_reporting_towns] 876 ['MILLINOCKET'] 877 878 >>> unit = ReportingUnit.from_strings('MILLINOCKET TWPS', 'PEN') 879 >>> [town.name for town in unit.specified_reporting_towns] 880 [] 881 """ 882 return [t for t in self.reporting_towns if type(t) != UnspecifiedGroup]
List of reporting units that are not unspecified groups.
Examples:
>>> unit = ReportingUnit.from_strings('WYMAN TWP/SPRING LAKE TWP', 'FRA') >>> [town.name for town in unit.specified_reporting_towns] ['WYMAN TWP', 'SPRING LAKE TWP']>>> unit = ReportingUnit.from_strings('WYMAN TWP (CARRABASSETT VALLEY)', 'FRA') >>> [town.name for town in unit.specified_reporting_towns] ['WYMAN TWP']>>> unit = ReportingUnit.from_strings('MILLINOCKET/TWPS', 'PEN') >>> [town.name for town in unit.specified_reporting_towns] ['MILLINOCKET']>>> unit = ReportingUnit.from_strings('MILLINOCKET TWPS', 'PEN') >>> [town.name for town in unit.specified_reporting_towns] []
884 @cached_property 885 def unspecified_groups(self) -> List[ResultGeo]: 886 """ 887 List of reporting units that are unspecified groups. 888 889 Examples: 890 >>> unit = ReportingUnit.from_strings('MILLINOCKET/PEN TWPS', 'PEN') 891 >>> groups = unit.unspecified_groups 892 >>> [group.name for group in groups] 893 ['UNSPECIFIED MILLINOCKET TWPS [PEN]'] 894 >>> [group.canonical_name for group in groups] 895 ['Unspecified Penobscot County Twps'] 896 >>> [group.group_registration_town.canonical_name for group in groups] 897 ['Millinocket'] 898 """ 899 return [t for t in self.reporting_towns if type(t) == UnspecifiedGroup]
List of reporting units that are unspecified groups.
Examples:
>>> unit = ReportingUnit.from_strings('MILLINOCKET/PEN TWPS', 'PEN') >>> groups = unit.unspecified_groups >>> [group.name for group in groups] ['UNSPECIFIED MILLINOCKET TWPS [PEN]'] >>> [group.canonical_name for group in groups] ['Unspecified Penobscot County Twps'] >>> [group.group_registration_town.canonical_name for group in groups] ['Millinocket']
901 @cached_property 902 def has_unspecified_group(self) -> bool: 903 """ 904 Return True if the result object includes an unspecified group, else False. 905 906 Examples: 907 >>> unit = ReportingUnit.from_strings('MEDWAY/TOWNSHIPS', 'PEN') 908 >>> unit.has_unspecified_group 909 True 910 911 >>> unit = ReportingUnit.from_strings('ADAMSTOWN/LOWER CUPSUPTIC TWPS (RANGELEY)', 'OXF') 912 >>> unit.has_unspecified_group 913 False 914 915 >>> unit = ReportingUnit.from_strings('MILLINOCKET PISCATAQUIS TWPS', 'PIS') 916 >>> unit.has_unspecified_group 917 True 918 919 >>> unit = ReportingUnit.from_strings('MILLINOCKET/PEN TWPS', 'PEN') 920 >>> unit.has_unspecified_group 921 True 922 923 >>> unit = ReportingUnit.from_strings('LEXINGTON & SPRING LAKE TWPS', 'SOM') 924 >>> unit.has_unspecified_group 925 False 926 """ 927 reporting = self.result_string.reporting_town_names 928 registration = self.result_string.registration_town_names 929 group_name = ' '.join(filter(None, [*registration, *reporting])) 930 931 if len(registration) > 1: 932 return False 933 elif len(reporting) in (1, 2) and UNSPECIFIED_FLAG in reporting: 934 return True 935 elif all(UNSPECIFIED_FLAG in town for town in reporting): 936 return True 937 elif MULTI_COUNTY_PATTERN.match(group_name): 938 return True 939 else: 940 return False
Return True if the result object includes an unspecified group, else False.
Examples:
>>> unit = ReportingUnit.from_strings('MEDWAY/TOWNSHIPS', 'PEN') >>> unit.has_unspecified_group True>>> unit = ReportingUnit.from_strings('ADAMSTOWN/LOWER CUPSUPTIC TWPS (RANGELEY)', 'OXF') >>> unit.has_unspecified_group False>>> unit = ReportingUnit.from_strings('MILLINOCKET PISCATAQUIS TWPS', 'PIS') >>> unit.has_unspecified_group True>>> unit = ReportingUnit.from_strings('MILLINOCKET/PEN TWPS', 'PEN') >>> unit.has_unspecified_group True>>> unit = ReportingUnit.from_strings('LEXINGTON & SPRING LAKE TWPS', 'SOM') >>> unit.has_unspecified_group False
942 @cached_property 943 def is_matched(self) -> bool: 944 """True if every named geography in this unit matched a known town.""" 945 if not self.result_string.exists: 946 return False 947 else: 948 tokens = [*self.reporting_towns, *self.registration_towns] 949 return bool(tokens) and all(t.is_matched for t in tokens)
True if every named geography in this unit matched a known town.
951 def to_dict(self) -> dict[str]: 952 return { 953 'raw_str': self.raw_string, 954 'formatted_str': self.formatted_string, 955 'reporting_str': self.reporting_string, 956 'registration_str': self.registration_string, 957 'reporting': { 958 'specified': [town.to_dict() for town in self.specified_reporting_towns], 959 'unspecified': [group.to_dict() for group in self.unspecified_groups] 960 }, 961 'registration': [town.to_dict() for town in self.registration_towns], 962 'county': asdict(self.county), 963 'is_matched': self.is_matched 964 }