2021-08-21 19:52:10 +02:00
|
|
|
// Package data is used to parse the insee number into a usable and human-readable data structure.
|
|
|
|
// All the required data is embedded in the package so no external file is required.
|
2021-08-18 19:01:17 +02:00
|
|
|
package data
|
|
|
|
|
|
|
|
import (
|
2021-08-19 19:18:09 +02:00
|
|
|
_ "embed"
|
2021-08-18 22:46:05 +02:00
|
|
|
"encoding/json"
|
2021-08-18 19:01:17 +02:00
|
|
|
"fmt"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2021-08-21 10:44:33 +02:00
|
|
|
var (
|
2021-08-21 19:52:10 +02:00
|
|
|
// Male is one of the possible values for InseeData.Gender.
|
|
|
|
Male = "homme"
|
|
|
|
// Female is the other possible value for InseeData.Gender.
|
|
|
|
Female = "femme"
|
|
|
|
// Unknown is a value used when determining the value of a string field in InseeData was not possible.
|
2021-08-21 10:44:33 +02:00
|
|
|
Unknown = "inconnu(e)"
|
2021-08-21 19:52:10 +02:00
|
|
|
// France is the only item in the default slice for InseeData.Countries.
|
|
|
|
France = "FRANCE"
|
2021-08-21 10:44:33 +02:00
|
|
|
)
|
2021-08-18 22:46:05 +02:00
|
|
|
|
2021-08-19 19:18:09 +02:00
|
|
|
//go:embed curated_data/countries.json
|
|
|
|
var rawCountries []byte
|
|
|
|
|
|
|
|
//go:embed curated_data/departments.json
|
|
|
|
var rawDepartments []byte
|
|
|
|
|
|
|
|
//go:embed curated_data/cities.json
|
|
|
|
var rawCities []byte
|
|
|
|
|
2021-08-21 19:52:10 +02:00
|
|
|
// InseeData contains human-readable data about the insee number used to construct it.
|
2021-08-18 19:01:17 +02:00
|
|
|
type InseeData struct {
|
2021-08-21 19:52:10 +02:00
|
|
|
// InseeNumber is the raw number, as given.
|
|
|
|
InseeNumber string `json:"insee_number"`
|
|
|
|
// Gender is either Male or Female.
|
|
|
|
Gender string `json:"gender"`
|
|
|
|
// Year of birth.
|
|
|
|
Year int `json:"year"`
|
|
|
|
// Month of birth.
|
|
|
|
Month time.Month `json:"month"`
|
|
|
|
// Department of birth, represented with its name.
|
|
|
|
Department string `json:"department"`
|
|
|
|
// City of birth, represented with its name.
|
|
|
|
City string `json:"city"`
|
|
|
|
// CityCode is the INSEE code of the City of birth.
|
|
|
|
CityCode string `json:"city_code"`
|
|
|
|
// Foreign is false if the person is born in France, true otherwise.
|
|
|
|
Foreign bool `json:"foreign"`
|
|
|
|
// Countries is the list of country names matching the CountryCode.
|
|
|
|
// Some country codes may match multiple countries, so Countries is a slice.
|
|
|
|
// This is always set to `{"FRANCE"}` when Foreign is false.
|
|
|
|
Countries []string `json:"countries"`
|
|
|
|
// CountryCode is the code of the birth country.
|
|
|
|
CountryCode string `json:"country_code"`
|
|
|
|
// Continent of birth.
|
|
|
|
Continent string `json:"continent"`
|
|
|
|
// OrderOfBirth is the order of birth of the person in the city or country (if Foreign) of birth at the year/month of birth.
|
|
|
|
// For example, 384 would mean that the person is the 384th born in the specific city/country on the given year/month.
|
|
|
|
OrderOfBirth int `json:"order_of_birth"`
|
|
|
|
// ControlKey is the complement to 97 of the insee number (minus the last two digits) modulo 97.
|
|
|
|
ControlKey int `json:"control_key"`
|
2021-08-18 19:01:17 +02:00
|
|
|
}
|
|
|
|
|
2021-08-21 19:52:10 +02:00
|
|
|
// NewInseeData generates an InseeData struct, extracting the data into the relevant fields.
|
|
|
|
// The data is converted to a human-readable format before being stored.
|
|
|
|
// If a value can't be determined, the corresponding field is generally set to Unknown.
|
2021-08-18 19:01:17 +02:00
|
|
|
func NewInseeData(inseeNumber string) (*InseeData, error) {
|
|
|
|
if len(inseeNumber) != 15 {
|
2021-08-21 09:07:07 +02:00
|
|
|
return nil, fmt.Errorf("le numéro INSEE number must contain 15 characters")
|
2021-08-18 19:01:17 +02:00
|
|
|
}
|
|
|
|
num := inseeNumber
|
2021-08-18 22:46:05 +02:00
|
|
|
departmentCode := num[5:7]
|
|
|
|
cityCode := num[7:10]
|
|
|
|
if departmentCode == "97" || departmentCode == "98" {
|
|
|
|
departmentCode = num[5:8]
|
|
|
|
cityCode = num[8:10]
|
2021-08-18 19:01:17 +02:00
|
|
|
}
|
2021-08-18 22:46:05 +02:00
|
|
|
dep, err := strconv.Atoi(departmentCode)
|
|
|
|
if err != nil && departmentCode != "2A" && departmentCode != "2B" {
|
2021-08-18 19:01:17 +02:00
|
|
|
return nil, err
|
|
|
|
}
|
2021-08-19 19:18:09 +02:00
|
|
|
var city string
|
2021-08-18 22:46:05 +02:00
|
|
|
var department string
|
2021-08-21 19:52:10 +02:00
|
|
|
countries_ := []string{France}
|
2021-08-18 22:46:05 +02:00
|
|
|
countryCode := ""
|
|
|
|
continent := "Europe"
|
|
|
|
foreign := (dep >= 91 && dep <= 96) || dep == 99
|
|
|
|
if foreign {
|
2021-08-18 19:01:17 +02:00
|
|
|
foreign = true
|
2021-08-18 22:46:05 +02:00
|
|
|
countryCode = cityCode
|
|
|
|
cityCode = ""
|
2021-08-19 19:18:09 +02:00
|
|
|
countries_ = getCountry("99" + countryCode)
|
2021-08-18 22:46:05 +02:00
|
|
|
continent = continents[countryCode[0:1]]
|
|
|
|
department = Unknown
|
|
|
|
} else {
|
2021-08-19 19:18:09 +02:00
|
|
|
city = getCity(departmentCode + cityCode)
|
|
|
|
department = getDepartment(departmentCode)
|
2021-08-18 19:01:17 +02:00
|
|
|
}
|
|
|
|
order, err := strconv.Atoi(num[10:13])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
controlKey, err := strconv.Atoi(num[13:])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
monthInt, err := strconv.Atoi(num[3:5])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
month := time.Month(monthInt)
|
|
|
|
year, err := strconv.Atoi(num[1:3])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
year += 2000
|
|
|
|
now := time.Now()
|
2021-08-19 19:18:09 +02:00
|
|
|
birthday := time.Date(year, month, 1, 0, 0, 0, 0, now.Location())
|
2021-08-18 19:01:17 +02:00
|
|
|
if birthday.After(now) {
|
|
|
|
year -= 100
|
|
|
|
}
|
|
|
|
gender := num[0:1]
|
|
|
|
if gender == "1" {
|
2021-08-18 22:46:05 +02:00
|
|
|
gender = Male
|
2021-08-18 19:01:17 +02:00
|
|
|
} else {
|
2021-08-18 22:46:05 +02:00
|
|
|
gender = Female
|
2021-08-18 19:01:17 +02:00
|
|
|
}
|
|
|
|
return &InseeData{
|
|
|
|
InseeNumber: num,
|
|
|
|
Gender: gender,
|
|
|
|
Year: year,
|
|
|
|
Month: month,
|
|
|
|
Department: department,
|
|
|
|
City: city,
|
2021-08-18 22:46:05 +02:00
|
|
|
CityCode: cityCode,
|
2021-08-18 19:01:17 +02:00
|
|
|
Foreign: foreign,
|
2021-08-18 22:46:05 +02:00
|
|
|
Countries: countries_,
|
|
|
|
CountryCode: countryCode,
|
|
|
|
Continent: continent,
|
2021-08-18 19:01:17 +02:00
|
|
|
OrderOfBirth: order,
|
|
|
|
ControlKey: controlKey,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2021-08-21 19:52:10 +02:00
|
|
|
// IsValid returns true when the insee number is valid and false when not.
|
|
|
|
// The insee number is valid when it matches its ControlKey.
|
2021-08-18 19:01:17 +02:00
|
|
|
func (insee InseeData) IsValid() (bool, error) {
|
2021-08-18 22:46:05 +02:00
|
|
|
r := strings.NewReplacer(
|
|
|
|
"2A", "19",
|
|
|
|
"2B", "18",
|
|
|
|
)
|
|
|
|
num := r.Replace(insee.InseeNumber[:len(insee.InseeNumber)-2])
|
2021-08-18 19:01:17 +02:00
|
|
|
numInt, err := strconv.Atoi(num)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
code := 97 - (numInt % 97)
|
|
|
|
return code == insee.ControlKey, nil
|
|
|
|
}
|
|
|
|
|
2021-08-21 19:52:10 +02:00
|
|
|
// String returns a string representation of the InseeData in a human-readable format, suited for printing to stdout.
|
2021-08-18 19:01:17 +02:00
|
|
|
func (insee InseeData) String() string {
|
2021-08-18 22:46:05 +02:00
|
|
|
var result []string
|
|
|
|
result = append(result, insee.InseeNumber)
|
|
|
|
var line string
|
2021-08-18 19:01:17 +02:00
|
|
|
valid, err := insee.IsValid()
|
|
|
|
if err != nil {
|
2021-08-18 22:46:05 +02:00
|
|
|
line = "Erreur lors de la vérification de la validité : " + err.Error()
|
|
|
|
} else if valid {
|
|
|
|
line = "Le numéro est valide."
|
|
|
|
} else {
|
|
|
|
line = "Le numéro est invalide."
|
2021-08-18 19:01:17 +02:00
|
|
|
}
|
2021-08-18 22:46:05 +02:00
|
|
|
result = append(result, line)
|
|
|
|
var one, born string
|
|
|
|
if insee.Gender == Male {
|
|
|
|
one = "un"
|
|
|
|
born = "né"
|
2021-08-18 19:01:17 +02:00
|
|
|
} else {
|
2021-08-18 22:46:05 +02:00
|
|
|
one = "une"
|
|
|
|
born = "née"
|
2021-08-18 19:01:17 +02:00
|
|
|
}
|
2021-08-18 22:46:05 +02:00
|
|
|
line = fmt.Sprintf("Vous êtes %s %s, %s en %s probablement en %d.", one, insee.Gender, born, frenchMonth(insee.Month), insee.Year)
|
|
|
|
result = append(result, line)
|
2021-08-18 19:01:17 +02:00
|
|
|
var zoneType string
|
|
|
|
if insee.Foreign {
|
2021-08-18 22:46:05 +02:00
|
|
|
zoneType = "ce pays"
|
|
|
|
if len(insee.Countries) > 1 {
|
|
|
|
line = fmt.Sprintf("Vous êtes %s dans l'un de ces pays/territoires: %s (%s)", born, strings.Join(insee.Countries, ", "), insee.Continent)
|
|
|
|
} else if len(insee.Countries) == 1 {
|
|
|
|
line = fmt.Sprintf("Vous êtes %s en %s (%s).", born, insee.Countries[0], insee.Continent)
|
2021-08-18 19:01:17 +02:00
|
|
|
} else {
|
2021-08-18 22:46:05 +02:00
|
|
|
line = fmt.Sprintf("Vous êtes %s dans un pays inconnu, portant l'identifiant %s.", born, insee.CountryCode)
|
2021-08-18 19:01:17 +02:00
|
|
|
}
|
|
|
|
} else {
|
2021-08-18 22:46:05 +02:00
|
|
|
zoneType = "cette ville"
|
2021-08-19 19:18:09 +02:00
|
|
|
if insee.City != Unknown {
|
|
|
|
line = fmt.Sprintf("Vous êtes %s à %s (%s, France)", born, insee.City, insee.Department)
|
2021-08-18 19:01:17 +02:00
|
|
|
} else {
|
2021-08-18 22:46:05 +02:00
|
|
|
line = fmt.Sprintf("Vous êtes %s dans une ville inconnue portant l'identifiant %s dans le département \"%s\"", born, insee.CityCode, insee.Department)
|
2021-08-18 19:01:17 +02:00
|
|
|
}
|
|
|
|
}
|
2021-08-18 22:46:05 +02:00
|
|
|
result = append(result, line)
|
|
|
|
line = fmt.Sprintf("Vous êtes la %de personne née dans %s le mois de votre naissance.", insee.OrderOfBirth, zoneType)
|
|
|
|
result = append(result, line)
|
|
|
|
lineB, err := json.Marshal(insee)
|
|
|
|
if err != nil {
|
|
|
|
line = "Erreur lors de la conversion en JSON: " + err.Error()
|
|
|
|
} else {
|
|
|
|
line = string(lineB)
|
|
|
|
}
|
|
|
|
result = append(result, line)
|
|
|
|
return strings.Join(result, "\n")
|
2021-08-18 19:01:17 +02:00
|
|
|
}
|
2021-08-19 19:18:09 +02:00
|
|
|
|
|
|
|
func getCity(cityCode string) string {
|
|
|
|
return getString(cityCode, rawCities)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getDepartment(departmentCode string) string {
|
|
|
|
return getString(departmentCode, rawDepartments)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getString(code string, rawData []byte) string {
|
|
|
|
cities := map[string]string{}
|
|
|
|
err := json.Unmarshal(rawData, &cities)
|
|
|
|
if err != nil {
|
|
|
|
return Unknown
|
|
|
|
}
|
|
|
|
item, present := cities[code]
|
|
|
|
if present {
|
|
|
|
return item
|
|
|
|
} else {
|
|
|
|
return Unknown
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getCountry(code string) []string {
|
|
|
|
cities := map[string][]string{}
|
|
|
|
err := json.Unmarshal(rawCountries, &cities)
|
|
|
|
if err != nil {
|
|
|
|
return []string{}
|
|
|
|
}
|
|
|
|
city, present := cities[code]
|
|
|
|
if present {
|
|
|
|
return city
|
|
|
|
} else {
|
|
|
|
return []string{}
|
|
|
|
}
|
|
|
|
}
|