1045 lines
35 KiB
PowerShell
1045 lines
35 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[string]$OutputPath = "..\\Tornado3_2026Election\\Assets\\Data\\pre_election_history.json",
|
|
[string]$ServiceKey = $env:NEC_SERVICE_KEY
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
$ProgressPreference = "SilentlyContinue"
|
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
|
|
if ([string]::IsNullOrWhiteSpace($ServiceKey))
|
|
{
|
|
throw "NEC_SERVICE_KEY environment variable or -ServiceKey is required."
|
|
}
|
|
|
|
function Decode-Text {
|
|
param([string]$Value)
|
|
|
|
return [uri]::UnescapeDataString($Value)
|
|
}
|
|
|
|
function Get-FirstValue {
|
|
param(
|
|
[System.Collections.IDictionary]$Table,
|
|
[string[]]$Keys
|
|
)
|
|
|
|
foreach ($key in $Keys)
|
|
{
|
|
if ($Table.Contains($key) -and $null -ne $Table[$key])
|
|
{
|
|
$value = [string]$Table[$key]
|
|
if (-not [string]::IsNullOrWhiteSpace($value))
|
|
{
|
|
return $value
|
|
}
|
|
}
|
|
}
|
|
|
|
return [string]::Empty
|
|
}
|
|
|
|
function Get-XmlItems {
|
|
param([xml]$Xml)
|
|
|
|
if ($null -eq $Xml)
|
|
{
|
|
return @()
|
|
}
|
|
|
|
$itemNodes = $Xml.SelectNodes("//item")
|
|
if ($null -eq $itemNodes)
|
|
{
|
|
return @()
|
|
}
|
|
|
|
return @($itemNodes)
|
|
}
|
|
|
|
function Invoke-NecXml {
|
|
param(
|
|
[string]$ServicePath,
|
|
[hashtable]$Parameters
|
|
)
|
|
|
|
$pairs = [System.Collections.Generic.List[string]]::new()
|
|
$pairs.Add("serviceKey=$([uri]::EscapeDataString($ServiceKey))")
|
|
foreach ($key in ($Parameters.Keys | Sort-Object))
|
|
{
|
|
$rawValue = $Parameters[$key]
|
|
if ($null -eq $rawValue)
|
|
{
|
|
continue
|
|
}
|
|
|
|
$stringValue = [string]$rawValue
|
|
if ([string]::IsNullOrWhiteSpace($stringValue))
|
|
{
|
|
continue
|
|
}
|
|
|
|
$pairs.Add("$([uri]::EscapeDataString($key))=$([uri]::EscapeDataString($stringValue))")
|
|
}
|
|
|
|
$uri = "https://apis.data.go.kr/9760000/${ServicePath}?$($pairs -join '&')"
|
|
for ($attempt = 1; $attempt -le 3; $attempt++)
|
|
{
|
|
try
|
|
{
|
|
[xml]$xml = (Invoke-WebRequest -UseBasicParsing -Uri $uri).Content
|
|
$resultCode = [string]$xml.response.header.resultCode
|
|
if (-not [string]::IsNullOrWhiteSpace($resultCode) -and $resultCode -notin @("00", "INFO-00", "INFO-03"))
|
|
{
|
|
$resultMessage = [string]$xml.response.header.resultMsg
|
|
throw "API failure [$resultCode] $resultMessage"
|
|
}
|
|
|
|
return $xml
|
|
}
|
|
catch
|
|
{
|
|
if ($attempt -ge 3)
|
|
{
|
|
throw
|
|
}
|
|
|
|
Start-Sleep -Milliseconds (250 * $attempt)
|
|
}
|
|
}
|
|
|
|
throw "Failed to call $ServicePath"
|
|
}
|
|
|
|
function Get-LocalElectionCycles {
|
|
param(
|
|
[System.Collections.IEnumerable]$CodeItems,
|
|
[string]$SgTypeCode
|
|
)
|
|
|
|
$localElectionPattern = '^' +
|
|
[regex]::Escape($(Decode-Text "%EC%A0%9C")) +
|
|
'(?<order>\d+)' +
|
|
[regex]::Escape($(Decode-Text "%ED%9A%8C%20%EC%A0%84%EA%B5%AD%EB%8F%99%EC%8B%9C%EC%A7%80%EB%B0%A9%EC%84%A0%EA%B1%B0")) +
|
|
'$'
|
|
|
|
$cycleMap = @{}
|
|
foreach ($item in $CodeItems)
|
|
{
|
|
$sgName = [string]$item.sgName
|
|
if ($sgName -notmatch $localElectionPattern)
|
|
{
|
|
continue
|
|
}
|
|
|
|
if ([string]$item.sgTypecode -ne $SgTypeCode)
|
|
{
|
|
continue
|
|
}
|
|
|
|
$sgId = [string]$item.sgId
|
|
if ([string]::IsNullOrWhiteSpace($sgId) -or [int]$sgId -gt 20220601)
|
|
{
|
|
continue
|
|
}
|
|
|
|
$order = [int]$Matches["order"]
|
|
$cycleMap[$order] = [pscustomobject]@{
|
|
Order = $order
|
|
Year = [int]$sgId.Substring(0, 4)
|
|
SgId = $sgId
|
|
SgTypeCode = $SgTypeCode
|
|
}
|
|
}
|
|
|
|
return @($cycleMap.Values | Sort-Object Order)
|
|
}
|
|
|
|
function New-SourceReference {
|
|
param(
|
|
[string]$Title,
|
|
[string]$Url
|
|
)
|
|
|
|
return [pscustomobject][ordered]@{
|
|
Title = $Title
|
|
Url = $Url
|
|
}
|
|
}
|
|
|
|
function Get-RegionApiNames {
|
|
param([object]$Region)
|
|
|
|
$names = [System.Collections.Generic.List[string]]::new()
|
|
$primaryName = Decode-Text $Region.DisplayName
|
|
if (-not [string]::IsNullOrWhiteSpace($primaryName))
|
|
{
|
|
$names.Add($primaryName)
|
|
}
|
|
|
|
if ($Region.Contains("ApiNames"))
|
|
{
|
|
foreach ($encodedName in $Region.ApiNames)
|
|
{
|
|
$decodedName = Decode-Text $encodedName
|
|
if (-not [string]::IsNullOrWhiteSpace($decodedName) -and -not $names.Contains($decodedName))
|
|
{
|
|
$names.Add($decodedName)
|
|
}
|
|
}
|
|
}
|
|
|
|
return @($names)
|
|
}
|
|
|
|
function New-HistoryRecord {
|
|
param(
|
|
[string]$ElectionType,
|
|
[string]$Key,
|
|
[string]$RegionKey,
|
|
[string]$RegionName,
|
|
[string]$DistrictName,
|
|
[string]$DisplayName
|
|
)
|
|
|
|
return [ordered]@{
|
|
ElectionType = $ElectionType
|
|
Key = $Key
|
|
RegionKey = $RegionKey
|
|
RegionName = $RegionName
|
|
DistrictName = $DistrictName
|
|
DisplayName = $DisplayName
|
|
TurnoutHistory = [System.Collections.Generic.List[object]]::new()
|
|
WinnerHistory = [System.Collections.Generic.List[object]]::new()
|
|
}
|
|
}
|
|
|
|
function Convert-RecordForJson {
|
|
param([System.Collections.IDictionary]$Record)
|
|
|
|
return [ordered]@{
|
|
ElectionType = $Record.ElectionType
|
|
Key = $Record.Key
|
|
RegionKey = $Record.RegionKey
|
|
RegionName = $Record.RegionName
|
|
DistrictName = $Record.DistrictName
|
|
DisplayName = $Record.DisplayName
|
|
TurnoutHistory = @($Record.TurnoutHistory | Sort-Object ElectionOrder)
|
|
WinnerHistory = @($Record.WinnerHistory | Sort-Object ElectionOrder)
|
|
}
|
|
}
|
|
|
|
function Add-HistoryEntry {
|
|
param(
|
|
[System.Collections.Generic.List[object]]$Target,
|
|
[object]$Entry
|
|
)
|
|
|
|
if ($null -eq $Entry)
|
|
{
|
|
return
|
|
}
|
|
|
|
$existing = $Target | Where-Object { $_.ElectionOrder -eq $Entry.ElectionOrder } | Select-Object -First 1
|
|
if ($null -eq $existing)
|
|
{
|
|
$Target.Add($Entry)
|
|
}
|
|
}
|
|
|
|
function Normalize-CompactText {
|
|
param([string]$Value)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Value))
|
|
{
|
|
return [string]::Empty
|
|
}
|
|
|
|
return ($Value -replace '\s+', [string]::Empty).Trim()
|
|
}
|
|
|
|
function Normalize-BasicDistrictToken {
|
|
param([string]$Value)
|
|
|
|
$normalized = Normalize-CompactText -Value $Value
|
|
if ([string]::IsNullOrWhiteSpace($normalized))
|
|
{
|
|
return [string]::Empty
|
|
}
|
|
|
|
$normalized = $normalized.Replace($(Decode-Text "%EA%B5%AC%EC%B2%AD%EC%9E%A5"), $(Decode-Text "%EA%B5%AC"))
|
|
$normalized = $normalized.Replace($(Decode-Text "%EA%B5%B0%EC%88%98"), $(Decode-Text "%EA%B5%B0"))
|
|
$normalized = $normalized.Replace($(Decode-Text "%EC%8B%9C%EC%9E%A5"), $(Decode-Text "%EC%8B%9C"))
|
|
$normalized = $normalized.Replace($(Decode-Text "%EA%B5%90%EC%9C%A1%EA%B0%90"), [string]::Empty)
|
|
return $normalized.Trim()
|
|
}
|
|
|
|
function New-OfficialWinnerEntry {
|
|
param(
|
|
[pscustomobject]$Cycle,
|
|
[object]$Item,
|
|
[string]$SourceUrl
|
|
)
|
|
|
|
if ($null -eq $Item)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
$name = ([string]$Item.name).Trim()
|
|
if ([string]::IsNullOrWhiteSpace($name))
|
|
{
|
|
return $null
|
|
}
|
|
|
|
return [ordered]@{
|
|
ElectionOrder = $Cycle.Order
|
|
Year = $Cycle.Year
|
|
Name = $name
|
|
Party = ([string]$Item.jdName).Trim()
|
|
Note = [string]::Empty
|
|
SourceUrl = $SourceUrl
|
|
}
|
|
}
|
|
|
|
function New-OfficialTurnoutEntry {
|
|
param(
|
|
[pscustomobject]$Cycle,
|
|
[int]$Electors,
|
|
[int]$Votes,
|
|
[string]$SourceUrl
|
|
)
|
|
|
|
if ($Electors -le 0 -or $Votes -le 0)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
return [ordered]@{
|
|
ElectionOrder = $Cycle.Order
|
|
Year = $Cycle.Year
|
|
Electors = $Electors
|
|
Votes = $Votes
|
|
TurnoutRate = [Math]::Round(($Votes * 100.0) / $Electors, 1, [MidpointRounding]::AwayFromZero)
|
|
SourceUrl = $SourceUrl
|
|
}
|
|
}
|
|
|
|
function Resolve-BasicTurnoutSnapshot {
|
|
param(
|
|
[string]$DistrictName,
|
|
[object[]]$TurnoutItems
|
|
)
|
|
|
|
$normalizedTarget = Normalize-CompactText -Value $DistrictName
|
|
if ([string]::IsNullOrWhiteSpace($normalizedTarget))
|
|
{
|
|
return $null
|
|
}
|
|
|
|
$exactMatches = @(
|
|
$TurnoutItems |
|
|
Where-Object { (Normalize-CompactText -Value ([string]$_.wiwName)) -eq $normalizedTarget }
|
|
)
|
|
|
|
$matchedItems = @(
|
|
if ($exactMatches.Count -gt 0)
|
|
{
|
|
$exactMatches
|
|
}
|
|
else
|
|
{
|
|
@(
|
|
$TurnoutItems |
|
|
Where-Object {
|
|
$normalizedWiwName = Normalize-CompactText -Value ([string]$_.wiwName)
|
|
-not [string]::IsNullOrWhiteSpace($normalizedWiwName) -and
|
|
$normalizedWiwName.StartsWith($normalizedTarget, [System.StringComparison]::Ordinal)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
|
|
if ($matchedItems.Count -eq 0)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
$electors = 0
|
|
$votes = 0
|
|
foreach ($matchedItem in $matchedItems)
|
|
{
|
|
$electors += Parse-PositiveInt -Value ([string]$matchedItem.totSunsu)
|
|
$votes += Parse-PositiveInt -Value ([string]$matchedItem.totTusu)
|
|
}
|
|
|
|
if ($electors -le 0 -or $votes -le 0)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
return [pscustomobject]@{
|
|
Electors = $electors
|
|
Votes = $votes
|
|
}
|
|
}
|
|
|
|
$yearByOrder = @{
|
|
1 = 1995
|
|
2 = 1998
|
|
3 = 2002
|
|
4 = 2006
|
|
5 = 2010
|
|
6 = 2014
|
|
7 = 2018
|
|
8 = 2022
|
|
}
|
|
|
|
$governorType = Decode-Text "%EA%B4%91%EC%97%AD%EB%8B%A8%EC%B2%B4%EC%9E%A5"
|
|
$educationType = Decode-Text "%EA%B5%90%EC%9C%A1%EA%B0%90"
|
|
$basicType = Decode-Text "%EA%B8%B0%EC%B4%88%EB%8B%A8%EC%B2%B4%EC%9E%A5"
|
|
$redirectKeyword = Decode-Text "%EB%84%98%EA%B2%A8%EC%A3%BC%EA%B8%B0"
|
|
$headingPrefix = Decode-Text "%EC%A0%9C"
|
|
$headingSuffix = Decode-Text "%ED%9A%8C%20%EC%A0%84%EA%B5%AD%EB%8F%99%EC%8B%9C%EC%A7%80%EB%B0%A9%EC%84%A0%EA%B1%B0"
|
|
$winnerTemplatePrefix = Decode-Text "%EB%8B%B9%EC%84%A0%20%EC%84%A0%EA%B1%B0%EA%B2%B0%EA%B3%BC"
|
|
$turnoutStartTemplatePrefix = Decode-Text "%EC%84%A0%EA%B1%B0%EA%B2%B0%EA%B3%BC%20%EC%8B%9C%EC%9E%91"
|
|
$turnoutTotalTemplatePrefix = Decode-Text "%EC%84%A0%EA%B1%B0%EA%B2%B0%EA%B3%BC%20%ED%95%A9%EA%B3%84"
|
|
$candidateKey = Decode-Text "%ED%9B%84%EB%B3%B4"
|
|
$candidateNameKey = Decode-Text "%ED%9B%84%EB%B3%B4%EB%AA%85"
|
|
$partyKey = Decode-Text "%EC%A0%95%EB%8B%B9"
|
|
$partyNameKey = Decode-Text "%EC%A0%95%EB%8B%B9%EB%AA%85"
|
|
$noteKey = Decode-Text "%EB%B9%84%EA%B3%A0"
|
|
$electorsKey = Decode-Text "%EC%9C%A0%EA%B6%8C%EC%9E%90"
|
|
$totalElectorsKey = Decode-Text "%EC%B4%9D%EC%84%A0%EA%B1%B0%EC%9D%B8%EC%88%98"
|
|
$totalVotesKey = Decode-Text "%EC%B4%9D%ED%88%AC%ED%91%9C%EC%9E%90%EC%88%98"
|
|
$sumKey = Decode-Text "%ED%95%A9%EA%B3%84"
|
|
$totalLabel = Decode-Text "%ED%95%A9%EA%B3%84"
|
|
$electionHeadingPattern = [regex]::Escape($headingPrefix) + "(?<order>\d+)" + [regex]::Escape($headingSuffix)
|
|
|
|
$codeInfoSourceUrl = "https://www.data.go.kr/data/15000897/openapi.do"
|
|
$winnerSourceUrl = "https://www.data.go.kr/data/15000864/openapi.do"
|
|
$turnoutSourceUrl = "https://www.data.go.kr/data/15000900/openapi.do"
|
|
|
|
$officialSources = @(
|
|
(New-SourceReference -Title "NEC Code Info" -Url $codeInfoSourceUrl),
|
|
(New-SourceReference -Title "NEC Winner Info" -Url $winnerSourceUrl),
|
|
(New-SourceReference -Title "NEC Vote Turnout Info" -Url $turnoutSourceUrl)
|
|
)
|
|
|
|
$regions = @(
|
|
[ordered]@{ Key = "%EC%84%9C%EC%9A%B8"; DisplayName = "%EC%84%9C%EC%9A%B8%ED%8A%B9%EB%B3%84%EC%8B%9C"; Titles = @("%EC%84%9C%EC%9A%B8%ED%8A%B9%EB%B3%84%EC%8B%9C%EC%9E%A5%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EB%B6%80%EC%82%B0"; DisplayName = "%EB%B6%80%EC%82%B0%EA%B4%91%EC%97%AD%EC%8B%9C"; Titles = @("%EB%B6%80%EC%82%B0%EA%B4%91%EC%97%AD%EC%8B%9C%EC%9E%A5%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EB%8C%80%EA%B5%AC"; DisplayName = "%EB%8C%80%EA%B5%AC%EA%B4%91%EC%97%AD%EC%8B%9C"; Titles = @("%EB%8C%80%EA%B5%AC%EA%B4%91%EC%97%AD%EC%8B%9C%EC%9E%A5%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EC%9D%B8%EC%B2%9C"; DisplayName = "%EC%9D%B8%EC%B2%9C%EA%B4%91%EC%97%AD%EC%8B%9C"; Titles = @("%EC%9D%B8%EC%B2%9C%EA%B4%91%EC%97%AD%EC%8B%9C%EC%9E%A5%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EA%B4%91%EC%A3%BC"; DisplayName = "%EA%B4%91%EC%A3%BC%EA%B4%91%EC%97%AD%EC%8B%9C"; Titles = @("%EA%B4%91%EC%A3%BC%EA%B4%91%EC%97%AD%EC%8B%9C%EC%9E%A5%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EB%8C%80%EC%A0%84"; DisplayName = "%EB%8C%80%EC%A0%84%EA%B4%91%EC%97%AD%EC%8B%9C"; Titles = @("%EB%8C%80%EC%A0%84%EA%B4%91%EC%97%AD%EC%8B%9C%EC%9E%A5%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EC%9A%B8%EC%82%B0"; DisplayName = "%EC%9A%B8%EC%82%B0%EA%B4%91%EC%97%AD%EC%8B%9C"; Titles = @("%EC%9A%B8%EC%82%B0%EA%B4%91%EC%97%AD%EC%8B%9C%EC%9E%A5%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EC%84%B8%EC%A2%85"; DisplayName = "%EC%84%B8%EC%A2%85%ED%8A%B9%EB%B3%84%EC%9E%90%EC%B9%98%EC%8B%9C"; Titles = @("%EC%84%B8%EC%A2%85%ED%8A%B9%EB%B3%84%EC%9E%90%EC%B9%98%EC%8B%9C%EC%9E%A5%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EA%B2%BD%EA%B8%B0"; DisplayName = "%EA%B2%BD%EA%B8%B0%EB%8F%84"; Titles = @("%EA%B2%BD%EA%B8%B0%EB%8F%84%EC%A7%80%EC%82%AC%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EA%B0%95%EC%9B%90"; DisplayName = "%EA%B0%95%EC%9B%90%ED%8A%B9%EB%B3%84%EC%9E%90%EC%B9%98%EB%8F%84"; Titles = @("%EA%B0%95%EC%9B%90%ED%8A%B9%EB%B3%84%EC%9E%90%EC%B9%98%EB%8F%84%EC%A7%80%EC%82%AC%20%EC%84%A0%EA%B1%B0", "%EA%B0%95%EC%9B%90%EB%8F%84%EC%A7%80%EC%82%AC%20%EC%84%A0%EA%B1%B0"); ApiNames = @("%EA%B0%95%EC%9B%90%EB%8F%84") },
|
|
[ordered]@{ Key = "%EC%B6%A9%EB%B6%81"; DisplayName = "%EC%B6%A9%EC%B2%AD%EB%B6%81%EB%8F%84"; Titles = @("%EC%B6%A9%EC%B2%AD%EB%B6%81%EB%8F%84%EC%A7%80%EC%82%AC%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EC%B6%A9%EB%82%A8"; DisplayName = "%EC%B6%A9%EC%B2%AD%EB%82%A8%EB%8F%84"; Titles = @("%EC%B6%A9%EC%B2%AD%EB%82%A8%EB%8F%84%EC%A7%80%EC%82%AC%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EC%A0%84%EB%B6%81"; DisplayName = "%EC%A0%84%EB%B6%81%ED%8A%B9%EB%B3%84%EC%9E%90%EC%B9%98%EB%8F%84"; Titles = @("%EC%A0%84%EB%B6%81%ED%8A%B9%EB%B3%84%EC%9E%90%EC%B9%98%EB%8F%84%EC%A7%80%EC%82%AC%20%EC%84%A0%EA%B1%B0", "%EC%A0%84%EB%9D%BC%EB%B6%81%EB%8F%84%EC%A7%80%EC%82%AC%20%EC%84%A0%EA%B1%B0"); ApiNames = @("%EC%A0%84%EB%9D%BC%EB%B6%81%EB%8F%84") },
|
|
[ordered]@{ Key = "%EC%A0%84%EB%82%A8"; DisplayName = "%EC%A0%84%EB%9D%BC%EB%82%A8%EB%8F%84"; Titles = @("%EC%A0%84%EB%9D%BC%EB%82%A8%EB%8F%84%EC%A7%80%EC%82%AC%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EA%B2%BD%EB%B6%81"; DisplayName = "%EA%B2%BD%EC%83%81%EB%B6%81%EB%8F%84"; Titles = @("%EA%B2%BD%EC%83%81%EB%B6%81%EB%8F%84%EC%A7%80%EC%82%AC%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EA%B2%BD%EB%82%A8"; DisplayName = "%EA%B2%BD%EC%83%81%EB%82%A8%EB%8F%84"; Titles = @("%EA%B2%BD%EC%83%81%EB%82%A8%EB%8F%84%EC%A7%80%EC%82%AC%20%EC%84%A0%EA%B1%B0") },
|
|
[ordered]@{ Key = "%EC%A0%9C%EC%A3%BC"; DisplayName = "%EC%A0%9C%EC%A3%BC%ED%8A%B9%EB%B3%84%EC%9E%90%EC%B9%98%EB%8F%84"; Titles = @("%EC%A0%9C%EC%A3%BC%ED%8A%B9%EB%B3%84%EC%9E%90%EC%B9%98%EB%8F%84%EC%A7%80%EC%82%AC%20%EC%84%A0%EA%B1%B0", "%EC%A0%9C%EC%A3%BC%EB%8F%84%EC%A7%80%EC%82%AC%20%EC%84%A0%EA%B1%B0") }
|
|
)
|
|
|
|
function Get-WikiPageUrl {
|
|
param([string]$Title)
|
|
|
|
return "https://ko.wikipedia.org/wiki/$([uri]::EscapeDataString($Title.Replace(' ', '_')))"
|
|
}
|
|
|
|
function Get-WikiRawUrl {
|
|
param([string]$Title)
|
|
|
|
return "https://ko.wikipedia.org/w/index.php?title=$([uri]::EscapeDataString($Title.Replace(' ', '_')))&action=raw"
|
|
}
|
|
|
|
function Get-WikiPage {
|
|
param([object]$Region)
|
|
|
|
$attempted = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
$queue = [System.Collections.Generic.Queue[string]]::new()
|
|
foreach ($encodedTitle in $Region.Titles)
|
|
{
|
|
$title = Decode-Text $encodedTitle
|
|
if ($attempted.Add($title))
|
|
{
|
|
$queue.Enqueue($title)
|
|
}
|
|
}
|
|
|
|
while ($queue.Count -gt 0)
|
|
{
|
|
$title = $queue.Dequeue()
|
|
$rawUrl = Get-WikiRawUrl -Title $title
|
|
try
|
|
{
|
|
$response = Invoke-WebRequest -UseBasicParsing -Uri $rawUrl
|
|
}
|
|
catch
|
|
{
|
|
continue
|
|
}
|
|
|
|
$content = [string]$response.Content
|
|
if ($null -eq $content)
|
|
{
|
|
$content = [string]::Empty
|
|
}
|
|
|
|
$content = $content -replace "`r", [string]::Empty
|
|
if ([string]::IsNullOrWhiteSpace($content))
|
|
{
|
|
continue
|
|
}
|
|
|
|
$redirectPattern = '^\s*#(?:' + [regex]::Escape($redirectKeyword) + '|redirect)\s*\[\[(?<target>[^\]]+)\]\]'
|
|
if ($content -match $redirectPattern)
|
|
{
|
|
$redirectTarget = ($Matches["target"] -replace '\|.*$', [string]::Empty).Trim()
|
|
if (-not [string]::IsNullOrWhiteSpace($redirectTarget) -and $attempted.Add($redirectTarget))
|
|
{
|
|
$queue.Enqueue($redirectTarget)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
return [pscustomobject]@{
|
|
Title = $title
|
|
Content = $content
|
|
SourceUrl = Get-WikiPageUrl -Title $title
|
|
}
|
|
}
|
|
|
|
throw "Wikipedia page was not found: $($Region.Titles -join ', ')"
|
|
}
|
|
|
|
function Split-WikiTemplateFields {
|
|
param([string]$TemplateText)
|
|
|
|
$text = $TemplateText.Trim()
|
|
if ($text.StartsWith("{{", [System.StringComparison]::Ordinal))
|
|
{
|
|
$text = $text.Substring(2)
|
|
}
|
|
|
|
if ($text.EndsWith("}}", [System.StringComparison]::Ordinal))
|
|
{
|
|
$text = $text.Substring(0, $text.Length - 2)
|
|
}
|
|
|
|
$fields = [System.Collections.Generic.List[string]]::new()
|
|
$builder = [System.Text.StringBuilder]::new()
|
|
$braceDepth = 0
|
|
$bracketDepth = 0
|
|
|
|
for ($index = 0; $index -lt $text.Length; $index++)
|
|
{
|
|
if ($index + 1 -lt $text.Length)
|
|
{
|
|
$pair = $text.Substring($index, 2)
|
|
if ($pair -eq "{{")
|
|
{
|
|
$braceDepth++
|
|
[void]$builder.Append($pair)
|
|
$index++
|
|
continue
|
|
}
|
|
|
|
if ($pair -eq "}}")
|
|
{
|
|
if ($braceDepth -gt 0)
|
|
{
|
|
$braceDepth--
|
|
}
|
|
|
|
[void]$builder.Append($pair)
|
|
$index++
|
|
continue
|
|
}
|
|
|
|
if ($pair -eq "[[")
|
|
{
|
|
$bracketDepth++
|
|
[void]$builder.Append($pair)
|
|
$index++
|
|
continue
|
|
}
|
|
|
|
if ($pair -eq "]]")
|
|
{
|
|
if ($bracketDepth -gt 0)
|
|
{
|
|
$bracketDepth--
|
|
}
|
|
|
|
[void]$builder.Append($pair)
|
|
$index++
|
|
continue
|
|
}
|
|
}
|
|
|
|
if ($text[$index] -eq "|" -and $braceDepth -eq 0 -and $bracketDepth -eq 0)
|
|
{
|
|
$fields.Add($builder.ToString())
|
|
[void]$builder.Clear()
|
|
continue
|
|
}
|
|
|
|
[void]$builder.Append($text[$index])
|
|
}
|
|
|
|
$fields.Add($builder.ToString())
|
|
return $fields
|
|
}
|
|
|
|
function Get-WikiTemplateParameters {
|
|
param([string]$TemplateText)
|
|
|
|
$parameters = [ordered]@{}
|
|
$fields = Split-WikiTemplateFields -TemplateText $TemplateText
|
|
foreach ($field in ($fields | Select-Object -Skip 1))
|
|
{
|
|
$separatorIndex = $field.IndexOf("=")
|
|
if ($separatorIndex -lt 0)
|
|
{
|
|
continue
|
|
}
|
|
|
|
$name = $field.Substring(0, $separatorIndex).Trim()
|
|
$value = $field.Substring($separatorIndex + 1).Trim()
|
|
if (-not [string]::IsNullOrWhiteSpace($name))
|
|
{
|
|
$parameters[$name] = $value
|
|
}
|
|
}
|
|
|
|
return $parameters
|
|
}
|
|
|
|
function Normalize-WikiText {
|
|
param([string]$Value)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Value))
|
|
{
|
|
return [string]::Empty
|
|
}
|
|
|
|
$text = $Value
|
|
$text = [System.Text.RegularExpressions.Regex]::Replace($text, '(?is)<ref[^>]*>.*?</ref>', [string]::Empty)
|
|
$text = [System.Text.RegularExpressions.Regex]::Replace($text, '(?is)<ref[^>]*/\s*>', [string]::Empty)
|
|
$text = [System.Text.RegularExpressions.Regex]::Replace($text, '(?i)<br\s*/?>', ' ')
|
|
$text = $text.Replace(" ", " ")
|
|
|
|
while ($text -match '\[\[([^\]|]+)\|([^\]]+)\]\]')
|
|
{
|
|
$text = [System.Text.RegularExpressions.Regex]::Replace($text, '\[\[([^\]|]+)\|([^\]]+)\]\]', '$2')
|
|
}
|
|
|
|
$text = [System.Text.RegularExpressions.Regex]::Replace($text, '\[\[([^\]]+)\]\]', '$1')
|
|
$text = [System.Text.RegularExpressions.Regex]::Replace($text, '\{\{(?:nowrap|small)\|([^{}]+)\}\}', '$1')
|
|
$text = [System.Text.RegularExpressions.Regex]::Replace($text, '\{\{lang\|[^|]+\|([^{}]+)\}\}', '$1')
|
|
|
|
while ($text -match '\{\{[^{}]+\}\}')
|
|
{
|
|
$text = [System.Text.RegularExpressions.Regex]::Replace($text, '\{\{[^{}]+\}\}', [string]::Empty)
|
|
}
|
|
|
|
$text = $text.Replace("'''", [string]::Empty)
|
|
$text = $text.Replace("''", [string]::Empty)
|
|
$text = [System.Text.RegularExpressions.Regex]::Replace($text, '\s+', ' ')
|
|
return $text.Trim()
|
|
}
|
|
|
|
function Parse-PositiveInt {
|
|
param([string]$Value)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Value))
|
|
{
|
|
return 0
|
|
}
|
|
|
|
$digits = $Value -replace '[^0-9]', [string]::Empty
|
|
if ([string]::IsNullOrWhiteSpace($digits))
|
|
{
|
|
return 0
|
|
}
|
|
|
|
return [int]$digits
|
|
}
|
|
|
|
function Get-ElectionSections {
|
|
param([string]$Content)
|
|
|
|
$sectionMatches = [System.Text.RegularExpressions.Regex]::Matches($Content, '(?m)^===\s*(?<heading>.+?)\s*===\s*$')
|
|
$sections = @{}
|
|
for ($index = 0; $index -lt $sectionMatches.Count; $index++)
|
|
{
|
|
$heading = $sectionMatches[$index].Groups["heading"].Value
|
|
if ($heading -notmatch $electionHeadingPattern)
|
|
{
|
|
continue
|
|
}
|
|
|
|
$order = [int]$Matches["order"]
|
|
$start = $sectionMatches[$index].Index + $sectionMatches[$index].Length
|
|
$end = if ($index + 1 -lt $sectionMatches.Count) { $sectionMatches[$index + 1].Index } else { $Content.Length }
|
|
$sections[$order] = $Content.Substring($start, $end - $start)
|
|
}
|
|
|
|
return $sections
|
|
}
|
|
|
|
function Convert-ToWinnerEntry {
|
|
param(
|
|
[int]$ElectionOrder,
|
|
[string]$SectionText,
|
|
[string]$SourceUrl
|
|
)
|
|
|
|
$winnerPattern = '\{\{' + [regex]::Escape($winnerTemplatePrefix) + '[^\n]*'
|
|
$winnerMatch = [System.Text.RegularExpressions.Regex]::Match($SectionText, $winnerPattern)
|
|
if (-not $winnerMatch.Success)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
$parameters = Get-WikiTemplateParameters -TemplateText $winnerMatch.Value
|
|
$name = Normalize-WikiText -Value (Get-FirstValue -Table $parameters -Keys @($candidateKey, $candidateNameKey))
|
|
if ([string]::IsNullOrWhiteSpace($name))
|
|
{
|
|
return $null
|
|
}
|
|
|
|
return [ordered]@{
|
|
ElectionOrder = $ElectionOrder
|
|
Year = $yearByOrder[$ElectionOrder]
|
|
Name = $name
|
|
Party = Normalize-WikiText -Value (Get-FirstValue -Table $parameters -Keys @($partyKey, $partyNameKey))
|
|
Note = Normalize-WikiText -Value (Get-FirstValue -Table $parameters -Keys @($noteKey))
|
|
SourceUrl = $SourceUrl
|
|
}
|
|
}
|
|
|
|
function Convert-ToTurnoutEntry {
|
|
param(
|
|
[int]$ElectionOrder,
|
|
[string]$SectionText,
|
|
[string]$SourceUrl
|
|
)
|
|
|
|
if ($ElectionOrder -lt 3)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
$startPattern = '\{\{' + [regex]::Escape($turnoutStartTemplatePrefix) + '[^\n]*'
|
|
$totalPattern = '\{\{' + [regex]::Escape($turnoutTotalTemplatePrefix) + '[^\n]*'
|
|
$startMatch = [System.Text.RegularExpressions.Regex]::Match($SectionText, $startPattern)
|
|
$totalMatch = [System.Text.RegularExpressions.Regex]::Match($SectionText, $totalPattern)
|
|
if (-not $startMatch.Success -or -not $totalMatch.Success)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
$startParameters = Get-WikiTemplateParameters -TemplateText $startMatch.Value
|
|
$totalParameters = Get-WikiTemplateParameters -TemplateText $totalMatch.Value
|
|
$electors = Parse-PositiveInt -Value (Get-FirstValue -Table $startParameters -Keys @($electorsKey, $totalElectorsKey))
|
|
$votes = Parse-PositiveInt -Value (Get-FirstValue -Table $totalParameters -Keys @($sumKey, $totalVotesKey))
|
|
if ($electors -le 0 -or $votes -le 0)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
return [ordered]@{
|
|
ElectionOrder = $ElectionOrder
|
|
Year = $yearByOrder[$ElectionOrder]
|
|
Electors = $electors
|
|
Votes = $votes
|
|
TurnoutRate = [Math]::Round(($votes * 100.0) / $electors, 1, [MidpointRounding]::AwayFromZero)
|
|
SourceUrl = $SourceUrl
|
|
}
|
|
}
|
|
|
|
if (-not [System.IO.Path]::IsPathRooted($OutputPath))
|
|
{
|
|
$OutputPath = Join-Path $PSScriptRoot $OutputPath
|
|
}
|
|
|
|
$OutputPath = [System.IO.Path]::GetFullPath($OutputPath)
|
|
$outputDirectory = Split-Path -Parent $OutputPath
|
|
if (-not [string]::IsNullOrWhiteSpace($outputDirectory) -and -not (Test-Path -LiteralPath $outputDirectory))
|
|
{
|
|
[void](New-Item -ItemType Directory -Path $outputDirectory)
|
|
}
|
|
|
|
$sources = [System.Collections.Generic.List[object]]::new()
|
|
foreach ($source in $officialSources)
|
|
{
|
|
$sources.Add($source)
|
|
}
|
|
|
|
$codeItems = [System.Collections.Generic.List[object]]::new()
|
|
$codePageNo = 1
|
|
$codePageSize = 100
|
|
while ($true)
|
|
{
|
|
$codeXml = Invoke-NecXml -ServicePath "CommonCodeService/getCommonSgCodeList" -Parameters @{
|
|
pageNo = $codePageNo
|
|
numOfRows = $codePageSize
|
|
}
|
|
|
|
$pageItems = Get-XmlItems -Xml $codeXml
|
|
foreach ($pageItem in $pageItems)
|
|
{
|
|
$codeItems.Add($pageItem)
|
|
}
|
|
|
|
$totalCount = Parse-PositiveInt -Value ([string]$codeXml.response.body.totalCount)
|
|
if ($pageItems.Count -lt $codePageSize -or ($codePageNo * $codePageSize) -ge $totalCount)
|
|
{
|
|
break
|
|
}
|
|
|
|
$codePageNo++
|
|
}
|
|
|
|
$localElectionCycles = Get-LocalElectionCycles -CodeItems $codeItems -SgTypeCode "0"
|
|
$basicCycles = @($localElectionCycles)
|
|
$educationCycles = @($localElectionCycles | Where-Object { $_.Order -ge 5 })
|
|
|
|
$winnerCache = @{}
|
|
$turnoutCache = @{}
|
|
|
|
function Get-WinnerItems {
|
|
param(
|
|
[string]$SgId,
|
|
[string]$SgTypeCode,
|
|
[string]$SdName
|
|
)
|
|
|
|
$cacheKey = "$SgId|$SgTypeCode|$SdName"
|
|
if (-not $winnerCache.ContainsKey($cacheKey))
|
|
{
|
|
$winnerCache[$cacheKey] = Get-XmlItems -Xml (Invoke-NecXml -ServicePath "WinnerInfoInqireService2/getWinnerInfoInqire" -Parameters @{
|
|
sgId = $SgId
|
|
sgTypecode = $SgTypeCode
|
|
sdName = $SdName
|
|
pageNo = 1
|
|
numOfRows = 400
|
|
})
|
|
}
|
|
|
|
return @($winnerCache[$cacheKey])
|
|
}
|
|
|
|
function Get-TurnoutItems {
|
|
param(
|
|
[string]$SgId,
|
|
[string]$SdName
|
|
)
|
|
|
|
$cacheKey = "$SgId|$SdName"
|
|
if (-not $turnoutCache.ContainsKey($cacheKey))
|
|
{
|
|
$turnoutCache[$cacheKey] = Get-XmlItems -Xml (Invoke-NecXml -ServicePath "VoteXmntckInfoInqireService2/getVoteSttusInfoInqire" -Parameters @{
|
|
sgId = $SgId
|
|
sgTypecode = 3
|
|
sdName = $SdName
|
|
pageNo = 1
|
|
numOfRows = 400
|
|
})
|
|
}
|
|
|
|
return @($turnoutCache[$cacheKey])
|
|
}
|
|
|
|
function Get-WinnerItemsForRegion {
|
|
param(
|
|
[pscustomobject]$Cycle,
|
|
[string]$SgTypeCode,
|
|
[object]$Region
|
|
)
|
|
|
|
foreach ($apiName in (Get-RegionApiNames -Region $Region))
|
|
{
|
|
$items = @(Get-WinnerItems -SgId $Cycle.SgId -SgTypeCode $SgTypeCode -SdName $apiName)
|
|
if ($items.Count -gt 0)
|
|
{
|
|
return @($items)
|
|
}
|
|
}
|
|
|
|
return @()
|
|
}
|
|
|
|
function Get-TurnoutItemsForRegion {
|
|
param(
|
|
[pscustomobject]$Cycle,
|
|
[object]$Region
|
|
)
|
|
|
|
foreach ($apiName in (Get-RegionApiNames -Region $Region))
|
|
{
|
|
$items = @(Get-TurnoutItems -SgId $Cycle.SgId -SdName $apiName)
|
|
if ($items.Count -gt 0)
|
|
{
|
|
return @($items)
|
|
}
|
|
}
|
|
|
|
return @()
|
|
}
|
|
|
|
$records = [System.Collections.Generic.List[object]]::new()
|
|
|
|
foreach ($region in $regions)
|
|
{
|
|
$regionKey = Decode-Text $region.Key
|
|
$regionDisplayName = Decode-Text $region.DisplayName
|
|
|
|
Write-Host ("Building governor history: {0}" -f $regionDisplayName)
|
|
$page = Get-WikiPage -Region $region
|
|
$sources.Add((New-SourceReference -Title $page.Title -Url $page.SourceUrl))
|
|
|
|
$sections = Get-ElectionSections -Content $page.Content
|
|
$record = New-HistoryRecord -ElectionType $governorType -Key $regionKey -RegionKey $regionKey -RegionName $regionDisplayName -DistrictName $regionDisplayName -DisplayName $regionDisplayName
|
|
|
|
foreach ($order in 1..8)
|
|
{
|
|
if (-not $sections.ContainsKey($order))
|
|
{
|
|
continue
|
|
}
|
|
|
|
Add-HistoryEntry -Target $record.WinnerHistory -Entry (Convert-ToWinnerEntry -ElectionOrder $order -SectionText $sections[$order] -SourceUrl $page.SourceUrl)
|
|
Add-HistoryEntry -Target $record.TurnoutHistory -Entry (Convert-ToTurnoutEntry -ElectionOrder $order -SectionText $sections[$order] -SourceUrl $page.SourceUrl)
|
|
}
|
|
|
|
$records.Add((Convert-RecordForJson -Record $record))
|
|
}
|
|
|
|
foreach ($region in $regions)
|
|
{
|
|
$regionKey = Decode-Text $region.Key
|
|
$regionDisplayName = Decode-Text $region.DisplayName
|
|
Write-Host ("Building education history: {0}" -f $regionDisplayName)
|
|
|
|
$record = New-HistoryRecord -ElectionType $educationType -Key $regionKey -RegionKey $regionKey -RegionName $regionDisplayName -DistrictName $regionDisplayName -DisplayName $regionDisplayName
|
|
foreach ($cycle in $educationCycles)
|
|
{
|
|
$winnerItems = Get-WinnerItemsForRegion -Cycle $cycle -SgTypeCode "11" -Region $region
|
|
$winnerItem = $winnerItems | Where-Object { -not [string]::IsNullOrWhiteSpace(([string]$_.name)) } | Select-Object -First 1
|
|
Add-HistoryEntry -Target $record.WinnerHistory -Entry (New-OfficialWinnerEntry -Cycle $cycle -Item $winnerItem -SourceUrl $winnerSourceUrl)
|
|
|
|
$turnoutItems = Get-TurnoutItemsForRegion -Cycle $cycle -Region $region
|
|
$totalItem = $turnoutItems | Where-Object { [string]$_.wiwName -eq $totalLabel } | Select-Object -First 1
|
|
if ($null -ne $totalItem)
|
|
{
|
|
$electors = Parse-PositiveInt -Value ([string]$totalItem.totSunsu)
|
|
$votes = Parse-PositiveInt -Value ([string]$totalItem.totTusu)
|
|
Add-HistoryEntry -Target $record.TurnoutHistory -Entry (New-OfficialTurnoutEntry -Cycle $cycle -Electors $electors -Votes $votes -SourceUrl $turnoutSourceUrl)
|
|
}
|
|
}
|
|
|
|
$records.Add((Convert-RecordForJson -Record $record))
|
|
}
|
|
|
|
foreach ($region in $regions)
|
|
{
|
|
$regionKey = Decode-Text $region.Key
|
|
$regionDisplayName = Decode-Text $region.DisplayName
|
|
Write-Host ("Building basic local history: {0}" -f $regionDisplayName)
|
|
|
|
$basicRecordsByKey = @{}
|
|
foreach ($cycle in $basicCycles)
|
|
{
|
|
$winnerItems = Get-WinnerItemsForRegion -Cycle $cycle -SgTypeCode "4" -Region $region
|
|
$turnoutItems = Get-TurnoutItemsForRegion -Cycle $cycle -Region $region
|
|
$turnoutDetails = @($turnoutItems | Where-Object { [string]$_.wiwName -ne $totalLabel })
|
|
|
|
foreach ($winnerItem in $winnerItems)
|
|
{
|
|
$districtName = ([string]$winnerItem.sggName).Trim()
|
|
if ([string]::IsNullOrWhiteSpace($districtName))
|
|
{
|
|
$districtName = ([string]$winnerItem.wiwName).Trim()
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($districtName))
|
|
{
|
|
continue
|
|
}
|
|
|
|
$districtKey = Normalize-BasicDistrictToken -Value $districtName
|
|
if ([string]::IsNullOrWhiteSpace($districtKey))
|
|
{
|
|
continue
|
|
}
|
|
|
|
$recordKey = "$regionKey|$districtKey"
|
|
if (-not $basicRecordsByKey.ContainsKey($recordKey))
|
|
{
|
|
$basicRecordsByKey[$recordKey] = New-HistoryRecord `
|
|
-ElectionType $basicType `
|
|
-Key $recordKey `
|
|
-RegionKey $regionKey `
|
|
-RegionName $regionDisplayName `
|
|
-DistrictName $districtName `
|
|
-DisplayName "$regionDisplayName $districtName"
|
|
}
|
|
|
|
$record = $basicRecordsByKey[$recordKey]
|
|
Add-HistoryEntry -Target $record.WinnerHistory -Entry (New-OfficialWinnerEntry -Cycle $cycle -Item $winnerItem -SourceUrl $winnerSourceUrl)
|
|
|
|
$turnoutSnapshot = Resolve-BasicTurnoutSnapshot -DistrictName $districtName -TurnoutItems $turnoutDetails
|
|
if ($null -ne $turnoutSnapshot)
|
|
{
|
|
Add-HistoryEntry -Target $record.TurnoutHistory -Entry (New-OfficialTurnoutEntry -Cycle $cycle -Electors $turnoutSnapshot.Electors -Votes $turnoutSnapshot.Votes -SourceUrl $turnoutSourceUrl)
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($record in ($basicRecordsByKey.Values | Sort-Object DisplayName))
|
|
{
|
|
$records.Add((Convert-RecordForJson -Record $record))
|
|
}
|
|
}
|
|
|
|
$deduplicatedSources = @($sources | Group-Object Url | ForEach-Object { $_.Group[0] } | Sort-Object Url)
|
|
$coverageNotes = @(
|
|
(Decode-Text "%EC%A0%84%EA%B5%AD%2017%EA%B0%9C%20%EC%8B%9C%EB%8F%84%20%EA%B4%91%EC%97%AD%EB%8B%A8%EC%B2%B4%EC%9E%A5%C2%B7%EA%B5%90%EC%9C%A1%EA%B0%90%20%EA%B8%B0%EC%A4%80%20%EC%A0%80%EC%9E%A5%ED%98%95%20%EC%82%AC%EC%A0%84%20%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9E%85%EB%8B%88%EB%8B%A4."),
|
|
(Decode-Text "%EA%B8%B0%EC%B4%88%EB%8B%A8%EC%B2%B4%EC%9E%A5%EC%9D%80%20%EC%A4%91%EC%95%99%EC%84%A0%EA%B1%B0%EA%B4%80%EB%A6%AC%EC%9C%84%EC%9B%90%ED%9A%8C%20OpenAPI%20%EA%B8%B0%EC%A4%80%20%EC%A0%84%EA%B5%AD%20%EC%84%A0%EA%B1%B0%EA%B5%AC%EB%A5%BC%20%EB%AF%B8%EB%A6%AC%20%EC%A0%80%EC%9E%A5%ED%95%A9%EB%8B%88%EB%8B%A4."),
|
|
(Decode-Text "%EA%B4%91%EC%97%AD%EB%8B%A8%EC%B2%B4%EC%9E%A5%20%EB%8B%B9%EC%84%A0%EC%9E%90%EB%8A%94%201995%EB%85%84%EB%B6%80%ED%84%B0%202022%EB%85%84%EA%B9%8C%EC%A7%80,%20%ED%88%AC%ED%91%9C%EC%9C%A8%EC%9D%80%202002%EB%85%84%EB%B6%80%ED%84%B0%202022%EB%85%84%EA%B9%8C%EC%A7%80%20%EC%A0%80%EC%9E%A5%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4."),
|
|
(Decode-Text "%EA%B5%90%EC%9C%A1%EA%B0%90%EC%9D%80%20%EC%A0%84%EA%B5%AD%20%EC%A7%81%EC%84%A0%EC%A0%9C%EA%B0%80%20%EC%A0%81%EC%9A%A9%EB%90%9C%202010%EB%85%84%EB%B6%80%ED%84%B0%202022%EB%85%84%EA%B9%8C%EC%A7%80%20%EC%A0%80%EC%9E%A5%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4."),
|
|
(Decode-Text "%EA%B8%B0%EC%B4%88%EB%8B%A8%EC%B2%B4%EC%9E%A5%EC%9D%98%20%EB%B6%84%ED%95%A0%20%EC%8B%9C%20%EC%84%A0%EA%B1%B0%EA%B5%AC(%EC%88%98%EC%9B%90%EC%8B%9C%C2%B7%EC%B0%BD%EC%9B%90%EC%8B%9C%20%EB%93%B1)%EB%8A%94%20%EB%8F%99%EC%9D%BC%20%EC%8B%9C%20%EB%8B%A8%EC%9C%84%EB%A1%9C%20%ED%88%AC%ED%91%9C%EC%9C%A8%EC%9D%84%20%ED%95%A9%EC%82%B0%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4.")
|
|
)
|
|
|
|
$catalog = [ordered]@{
|
|
Metadata = [ordered]@{
|
|
Version = "2026-04-18-national-history-mixed1"
|
|
GeneratedAt = (Get-Date -Format "yyyy-MM-dd")
|
|
CoverageNotes = $coverageNotes
|
|
Sources = $deduplicatedSources
|
|
}
|
|
Records = @($records)
|
|
}
|
|
|
|
$json = $catalog | ConvertTo-Json -Depth 12
|
|
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
|
[System.IO.File]::WriteAllText($OutputPath, $json, $utf8NoBom)
|
|
|
|
$jsonObject = $json | ConvertFrom-Json
|
|
$counts = $jsonObject.Records | Group-Object ElectionType | Sort-Object Name
|
|
Write-Host ("Saved pre-election history seed to {0}" -f $OutputPath)
|
|
foreach ($count in $counts)
|
|
{
|
|
Write-Host ("{0}: {1}" -f $count.Name, $count.Count)
|
|
}
|