Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PATH
specs:
dfe-reference-data (3.9.0)
activesupport
csv
tzinfo

GEM
Expand Down Expand Up @@ -50,6 +51,7 @@ GEM
connection_pool (2.5.4)
console (1.15.3)
fiber-local
csv (3.3.6)
declarative (0.0.20)
diff-lcs (1.6.2)
drb (2.2.3)
Expand Down
14 changes: 14 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ require 'rspec/core/rake_task'
require_relative 'lib/dfe/reference_data'
require_relative 'lib/dfe/reference_data/bigquery/importer'
require_relative 'lib/dfe/reference_data/bigquery/converter'
require_relative 'lib/dfe/reference_data/dqt/degree_types'
require_relative 'lib/dfe/reference_data/helpers/v2/country_list_compiler'

RSpec::Core::RakeTask.new(:spec)

Expand Down Expand Up @@ -114,3 +116,15 @@ task :convert_to_sqlite do
versioned_output_file = output_file.sub('.sqlite3', "_v#{version}.sqlite3")
DfE::ReferenceData::BigQuery::Converter.convert_to_sqlite(versioned_output_file, BIGQUERY_TABLES)
end

namespace :v2 do
desc 'Generate countries hash entries - one off script'
task :generate_countries_hash do
DfE::ReferenceData::Helpers::V2::CountryListCompiler.new.generate_countries_hash
end

desc 'Generate territories hash entries - one off script'
task :generate_territories_hash do
DfE::ReferenceData::Helpers::V2::CountryListCompiler.new.generate_territories_hash
end
end
1 change: 1 addition & 0 deletions dfe-reference-data.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Gem::Specification.new do |s|
s.add_development_dependency('sqlite3', '~> 1.4')
# rubocop:enable Gemspec/DevelopmentDependencies
s.add_dependency 'activesupport'
s.add_dependency 'csv'
s.add_dependency 'tzinfo'

s.metadata['rubygems_mfa_required'] = 'true'
Expand Down
12 changes: 12 additions & 0 deletions docs/decisions/0004-versioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Versioning

## Context and problem statement

Teams using the reference data datasets need to be able to rely on a specific version of the datasets. Up to now,
there were situations where the datasets were updated without notice and it was leading to the applications displaying
data that might not be expected.

## Solution

Solution is to add a version namespace (V2, V3, etc.). Version `V1` is omitted.
For example, updates to the countries list were introduced in version `V2`: `DfE::ReferenceData::V2::CountriesAndTerritories`.
56 changes: 56 additions & 0 deletions docs/lists_countries_and_territories.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,62 @@
- A mapping of ISO country/territory codes to country/territory names and nationalities.
- Postcode prefixes for the UK and Channel Islands grouped by country/territory.

### V2 list generation

The list for V2 is generated from CSV files located in `raw_data` directory. Using scripts helps with understanding process and assures better quality.
Those are one off scripts, already run to generate `V2` lists. They are included as an example approach how to generate ruby structures from different data sources,
as well keeping those sources for reference.

To generate ruby structure, that later was copied to countries as `DfE::ReferenceData::CountriesAndTerritories::V2::COUNTRIES`,
2 rake tasks were created:

`rake v2:generate_countries_hash`
`rake v2:generate_territories_hash`

### `DfE::ReferenceData::CountriesAndTerritories::V2::COUNTRIES`

```ruby
require 'dfe/reference_data/v2/countries_and_territories'
```
A mapping of Foreign, Commonwealth and Development Office country/territory codes to country/territory names.

Owner: Data insights team

Source:
[Geographical Countries](https://www.gov.uk/government/publications/geographical-names-and-information)
Last updated on 22nd December 2025.
Comment thread
mysteryGarlicPress marked this conversation as resolved.

Quality: Manually updated from Foreign, Commonwealth and Development Office data.

This list is [autocomplete compatible](autocomplete_compatability.md).

| Field | Type | Purpose |
| --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `id` | string | The country or territory's ISO code. |
| `name` | string | The human-readable name of the country or territory. |
| `official_name` | string | The human-readable full ‘official name’. Used when the formal version of a country’s name is needed. |
| `citizen_names` | string | The human-readable citizen names. They are not the legal names for the citizen, they do not relate to the citizen’s ethnicity. |

### `DfE::ReferenceData::CountriesAndTerritories::V2::TERRITORIES`

```ruby
require 'dfe/reference_data/v2/countries_and_territories'
```
A list of all countries and territories. It was previously included in reference data.
Additional list to include all possible territories, can be used for domiciles as well as source of countries that might not be recognised but nontheless can issue passport etc.

Owner: Data insights team

Source:
DFE reference data/verified using ISO data.

This list is [autocomplete compatible](autocomplete_compatability.md).

| Field | Type | Purpose |
| --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `id` | string | The country or territory's ISO code. |
| `name` | string | The human-readable name of the country or territory. |

### `DfE::ReferenceData::CountriesAndTerritories::COUNTRIES_AND_TERRITORIES`

```ruby
Expand Down
89 changes: 89 additions & 0 deletions lib/dfe/reference_data/helpers/v2/country_list_compiler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
require 'csv'
Comment thread
mysteryGarlicPress marked this conversation as resolved.

module DfE
module ReferenceData
module Helpers
module V2
class CountryListCompiler
attr_reader :all_countries_data, :legacy_codes, :fcdo_codes, :fcdo_codes_index, :legacy_codes_index

LegacyCountry = Struct.new(:code, :name)
FCDOCountry = Struct.new(:code, :name, :official_name, :citizen_names)
# current FCDO list
FCDO_COUNTRIES = CSV.read('raw_data/FCDO_Geographical_Names_Index_September_2025.csv', headers: false).map do |country|

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we're loading the CSVs when the class is required rather than when the helper is actually invoked?

For production code this is probably fine because it's a utility class, but loading files during require can make testing and reuse a little harder.

FCDOCountry.new(*country)
end
LEGACY_COUNTRIES_AND_TERRITORIES = CSV.read('raw_data/legacy_countries.csv').map do |country|
LegacyCountry.new(*country)
end

def initialize
@legacy_codes = LEGACY_COUNTRIES_AND_TERRITORIES.map(&:code)
@fcdo_codes = FCDO_COUNTRIES.map(&:code)
@domicile_codes = @legacy_codes - @fcdo_codes
build_all_countries_data

missing_codes = @fcdo_codes - @legacy_codes

raise "Missing legacy codes: #{missing_codes.join(', ')}" unless missing_codes.empty?
end

# columns:
# code, name, full_name, citizen_name, FCDO_list, Legacy list
def create_full_csv
build_all_countries_data
CSV.open('raw_data/full_countries_and_territories.csv', 'wb') do |csv|
csv << ['code', 'name', 'full_name', 'citizen_name', 'FCDO_list', 'Legacy_list', 'is_domicile']
@all_countries_data.each do |country|
csv << [country[:code], country[:name], country[:full_name], country[:citizen_name], country[:fcdo_list_name], country[:legacy_list_name], @domicile_codes.include?(country[:code])]
end
end
end

def generate_countries_hash
@fcdo_codes.each do |code|
country = @fcdo_codes_index[code]
puts "\"#{code}\" => { name: \"#{country.name}\", official_name: \"#{country.official_name}\", citizen_names: \"#{country.citizen_names}\" },"
end
end

def generate_territories_hash
@domicile_codes.each do |code|
country = @legacy_codes_index[code]
puts "\"#{code}\" => { name: \"#{country.name}\" },"
end
end

def build_all_countries_data
compile_all_countries_symbols
build_indexes
@all_countries_data = []
@all_codes.each do |code|
# code, name, full_name, citizen_name, FCDO_list, Legacy list
@all_countries_data << {
code: code,
name: @legacy_codes_index[code]&.name,
full_name: @fcdo_codes_index[code]&.official_name,
citizen_name: @fcdo_codes_index[code]&.citizen_names,
fcdo_list_name: @fcdo_codes_index[code]&.name,
legacy_list_name: @legacy_codes_index[code]&.name
}
end
end

def compile_all_countries_symbols
@all_codes = (@legacy_codes + @fcdo_codes).uniq
end

def build_indexes
@fcdo_codes_index = {}
@fcdo_codes.each { |code| @fcdo_codes_index[code] = FCDO_COUNTRIES.find { |country| country.code == code } }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small readability/performance suggestion: could these indexes be built directly from the collections rather than repeatedly calling find?

For example:

@fcdo_codes_index = FCDO_COUNTRIES.index_by(&:code)


@legacy_codes_index = {}
@legacy_codes.each { |code| @legacy_codes_index[code] = LEGACY_COUNTRIES_AND_TERRITORIES.find { |country| country.code == code } }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above:

@legacy_codes_index = LEGACY_COUNTRIES_AND_TERRITORIES.index_by(&:code)

end
end
end
end
end
end
Loading
Loading