Tras sortear los árboles como buenamente hemos podido en nuestro viaje en trineo, hemos llegado por fin al aeropuerto. Pero al ir a sacar la documentación para pasar el control de seguridad nos encontramos con que en vez del pasaporte nos hemos traído las credenciales del Polo Norte, y estas no son válidas para volar. Y para colmo hay una cola enorme para comprobar los pasaportes porque el lector electrónico tiene problemas para identificar si los los documentos tienen todos los campos obligatorios informados.

Viendo la oportunidad de matar dos pájaros de un tiro, nos ofrecemos, como no, a echar una mano para acelerar el proceso y ,de paso, no tener que volver al Polo Norte a por el pasaporte.

Parte 1

Los datos de entrada a nuestro proceso serán el fichero que contiene la definicion de los pasaportes. Cada uno de ellos son un conjunto de pares valor separados por espacios o por saltos de linea. Cada pasaporte se separa de los demás con dos saltos de linea. Este ejemplo contiene dos pasaportes

iyr:2013 ecl:amb cid:350 eyr:2023 pid:028048884
hcl:#cfa07d byr:1929

hcl:#ae17e1 iyr:2013
eyr:2024
ecl:brn pid:760753108 byr:1931
hgt:179cm

La primera misión es comprobar que los campos byr,iyr,eyr,hgt,hcl,ecl, y pid existan en cada pasaporte. El campo cid, que indica el código de país emisor del documento, nos lo vamos a saltar con lo cual conseguiremos acceder con nuestras credenciales del Polo Norte.

Dado el tipo de información que tenemos delante la idea que me viene a la cabeza inmediatamente es trasformar el fichero de pasaportes en una colección de objetos Pasaporte, para después comprobar cuales de ellos tienen las propiedades obligatorias informadas.

Lo haré en dos pasos: conversión del fichero a una colección de objetos Pasaporte y validación de los mismos

Lectura del fichero y obtención de la colección de objetos Passport

Utilizaré la clase PassportDatabase, que mediante su método read lee el fichero que se le indique, lo trocea por los dobles saltos de linea para separa la información de cada pasaporte y llama al constructor de la clase Passport pasándole el array de pares clave:valor correspondiente a ese pasaporte, que han sido troceados partiendo la información por espacios y saltos de linea simples:

import * as fs from "fs";

class Passport {

  constructor(fields: string[]) {
    fields
      .filter((f) => f) //Eliminar vacios
      .forEach((pair) => {
        const parts = pair.split(":");
        this[parts[0]] = parts[1];
      });
  }
}

class PassportDatabase {
  public read = (file: string): Passport[] =>
    fs
      .readFileSync(file, "utf8")
      .split("\r\n\r\n")
      .map((passport) => new Passport(passport.split(/\s|\r\n/)));
}

const passports = new PassportDatabase().read("./passports.txt");

Tras ejecutar este método obtendremos un array de objetos Passport, donde en cada uno se han creado propiedades para cada par clave:valor en su constructor mediante la asignación this[parts[0]] = parts[1];

Validación de los campos obligatorios

Para comprobar si un objeto tiene sus campos obligatorios informados vamos a inspeccionar la colección de propiedades que tiene definidas y la compararemos con la lista de campos que deben existir.

class Passport {
  private static mandatoryFields = [
    "byr",
    "iyr",
    "eyr",
    "hgt",
    "hcl",
    "ecl",
    "pid",
  ];

 public get hasValidFields() {
    //Comprobar si tiene todos los campos obligatorios
    const passportFields = Object.keys(this);
    return Passport.mandatoryFields.every((field) =>
      passportFields.includes(field)
    );
  }
}

Para ello he creado una propiedad hasValidFields en la que, mediante la llamada a la funcion Object.keys(this) obtengo todas las propiedades del objeto. Luego, mediante la funcion Array.every verifico que existan en la lista mandatoryFields que he definido a nivel de clase para hacerla estática y que se comparta entre todas las instancias de Passport. Solo queda contar los pasaportes que contienen todos los campos:

const passports = new PassportDatabase().read("./passports.txt");
console.log(
  "Answer:",
  passports.filter((passport) => passport.hasValidFields).length
);

Parte 2

Como era de esperar, la segunda parte es una extensión de la primera: no solo los pasaportes deben tener esos campos informados, sino que sus valores deben cumplir ciertas reglas.

    byr (Birth Year) - four digits; at least 1920 and at most 2002.
    iyr (Issue Year) - four digits; at least 2010 and at most 2020.
    eyr (Expiration Year) - four digits; at least 2020 and at most 2030.
    hgt (Height) - a number followed by either cm or in:
        If cm, the number must be at least 150 and at most 193.
        If in, the number must be at least 59 and at most 76.
    hcl (Hair Color) - a # followed by exactly six characters 0-9 or a-f.
    ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth.
    pid (Passport ID) - a nine-digit number, including leading zeroes.
    cid (Country ID) - ignored, missing or not.

Siguiendo el método de resolución de la primera parte vamos a escribir funciones para validar cada uno de los campos y luego las invocaremos de manera dinámica para verificar si el pasaporte es válido. Todas las reglas de validación se ajustan muy bien al uso de expresiones regulares, así que defino un juego de ellas y una funcion para cada campo:

class Passport {
  private static fourDigits = /^\d{4}$/;
  private static height = /^(\d+)(cm|in)$/;
  private static hairColor = /^#[0-9a-f]{6}$/;
  private static eyeColor = /^(amb|blu|brn|gry|grn|hzl|oth)$/;
  private static pid = /^\d{9}$/;

  //byr (Birth Year) - four digits; at least 1920 and at most 2002.
  private byrValid = (value) =>
    Passport.fourDigits.test(value) && value >= 1920 && value <= 2002;

  //iyr (Issue Year) - four digits; at least 2010 and at most 2020.
  private iyrValid = (value) =>
    Passport.fourDigits.test(value) && value >= 2010 && value <= 2020;

  //eyr (Expiration Year) - four digits; at least 2020 and at most 2030.
  private eyrValid = (value) =>
    Passport.fourDigits.test(value) && value >= 2020 && value <= 2030;

  /*    hgt (Height) - a number followed by either cm or in:
        If cm, the number must be at least 150 and at most 193.
        If in, the number must be at least 59 and at most 76.*/
  private hgtValid = (value) => {
    if (!Passport.height.test(value)) return false;

    const matches = value.match(Passport.height);
    return matches[2] == "cm"
      ? matches[1] >= 150 && matches[1] <= 193
      : matches[1] >= 59 && matches[1] <= 76;
  };

  //hcl (Hair Color) - a # followed by exactly six characters 0-9 or a-f.
  private hclValid = (value) => Passport.hairColor.test(value);

  //ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth.
  private eclValid = (value) => Passport.eyeColor.test(value);

  //pid (Passport ID) - a nine-digit number, including leading zeroes.
  private pidValid = (value) => Passport.pid.test(value);
}

Ahora solo queda escribir una propiedad en Passport que nos diga si los valores de sus campos son válidos, muy al estilo de la anterior:

public get isValid() {
    if (!this.hasValidFields) return false;

    return Passport.mandatoryFields.every((field) =>
      this[`${field}Valid`](this[field])
    );
  }

La instruccion this[`${field}Valid`](this[field]) es la que invoca a cada una de las funciones de validación. Como en el anterior problema, con Array.every aseguramos que todos los campos cumplan sus reglas.

La invocación, de nuevo, es tan sencilla como contar los pasaportes que cumplen las reglas:

console.log("Answer:", passports.filter((passport) => passport.isValid).length);

NOTA MENTAL PARA FUTURO: Lee detenidamente las especificaciones. Durante 45 minutos no he encontrado por qué el resultado de esta función era incorrecto, hasta que en la quinta relectura he visto que la regla para el año de nacimiento es que sea menor o igual a 2002 y no a 2020

Puedes descargar el código completo de este ejemplo desde GitHub: oddbytes.net/adventofcode
Free WordPress Themes, Free Android Games