loading...

Daily Challenge #67- Phone Directory

thepracticaldev profile image dev.to staff ・2 min read

John keeps a backup of his old personal phone book as a text file. On each line of the file he can find the phone number (formated as +X-abc-def-ghij where X stands for one or two digits), the corresponding name between < and > and the address.

Unfortunately, everything is mixed, things are not always in the same order; parts of lines are cluttered with non-alpha-numeric characters (except inside phone number and name).

Examples of John's phone book lines:

"/+1-541-754-3010 156 Alphand_St. <J Steeve>\n"

" 133, Green, Rd. <E Kustur> NY-56423 ;+1-541-914-3010!\n"

"<Anastasia> +48-421-674-8974 Via Quirinal Roma\n"

Could you help John with a program that, given the lines of his phone book and a phone number returns a string for this number: "Phone => num, Name => name, Address => adress"

Examples:

s = "/+1-541-754-3010 156 Alphand_St. <J Steeve>\n 133, Green, Rd. <E Kustur> NY-56423 ;+1-541-914-3010!\n"

phone(s, "1-541-754-3010") should return "Phone => 1-541-754-3010, Name => J Steeve, Address => 156 Alphand St."

It can happen that, for a few phone numbers, there are many people for a phone number -say nb-, then return :
"Error => Too many people: nb"

or it can happen that the number nb is not in the phone book, in that case return:
"Error => Not found: nb"


Today's challenge comes from g964 CodeWars, who has licensed redistribution of this challenge under the 2-Clause BSD License!

Want to propose a challenge idea for a future post? Email yo+challenge@dev.to with your suggestions!

Posted on by:

thepracticaldev profile

dev.to staff

@thepracticaldev

The hardworking team behind dev.to ❤️

Discussion

pic
Editor guide
 

I'm still a newbie with Clojure, but I already kind of enjoy it.

Disclaimer: may not be "The Clojure Way", I don't know.

(ns day-67-phone-directory
  (:require [clojure.string :refer [replace split-lines trim]]))

(def regexps
  "A map of record fields to regular expressions to match them.
  The first capture group is the meaningful part to extract."
  {:phone #"\+(\d+-\d+-\d+-\d+)" :name #"<([^>]+?)>"})

(defn- find-first-group
  "Find `regex` in `book`, then extract the first capture group."
  [book regex]
  (-> regex (re-find book) (get 1)))

(defn- remove-regex
  "Removes the match of `regex` from `book`."
  [book regex]
  (replace book regex ""))

(defn- clean
  "Cleans an `address` into a kinda-human-readable state."
  [address]
  (-> address
      (replace #"_" " ")
      (replace #"[^a-zA-Z0-9 .]" "")
      trim))

(defn- line->record
  "Parse a `line` to a book record.
  `:address` is the leftover part after extracting phone and name."
  [line]
  (let [[record address]
        (->> regexps keys
            (reduce (fn [[record rest] key]
                      [(assoc record key
                              (find-first-group rest (regexps key)))
                       (remove-regex rest (regexps key))])
                    [{} line]))]
    (assoc record :address (clean address))))

(defn phone
  "Returns information about `number` in `book`, or an error message.
  Result depends on how many people with the same `number` were found."
  [book number]
  (let [matching (->> book split-lines (map line->record)
                     (filter #(->> % :phone (= number))))]
    (case (count matching)
      0 (str "Error => Not found: " number)
      1 (let [record (first matching)]
          (format "Phone => %s, Name => %s, Address => %s"
                  (:phone record) (:name record) (:address record)))
      (str "Error => Too many people: " number))))
 

Not sure about most efficient, but here's one way to do it using c#.
A more efficient way would be to first parse the entire text into objects, and then only search the already parsed text.

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

public class Program
{
    public static void Main()
    {
        var s = @"/+1-541-754-3010 156 Alphand_St. <J Steeve>
            133, Green, Rd. <E Kustur> NY-56423 ;+1-541-914-3010!
            <Anastasia> +48-421-674-8974 Via Quirinal Roma
            <Too many> some text +48-421-674-8974 ";

        Console.WriteLine(phone(s, "1-541-754-3010"));  // return J Steeve
        Console.WriteLine(phone(s, "1-541-914-3010"));  // return E Kustur
        Console.WriteLine(phone(s, "48-421-674-8974")); // return Error => Too many people: nb
        Console.WriteLine(phone(s, "48-421-674-1234")); // return Error => Not found: nb
    }

    static string phone(string phoneBook, string phoneNumber)
    {
        var entries = new Dictionary<string, PhoneBookEntry>();
        var lines = phoneBook.Split('\n');
        foreach(var line in lines)
        {
            if(PhoneBookEntry.TryParse(line, out PhoneBookEntry entry) && entry.Phone == phoneNumber)
            {
                if(entries.ContainsKey(entry.Phone))
                {
                    return "Error => Too many people: nb";
                }
                entries[entry.Phone] = entry;
            }
        }
        if(entries.TryGetValue(phoneNumber, out PhoneBookEntry result))
        {
            return result.ToString();
        }
        return "Error => Not found: nb";
    }



    private class PhoneBookEntry
    {   
        private static Regex _phoneRe = new Regex(@"\+(\d{1,2}-\d{3}-\d{3}-\d{4})");
        private static Regex _nameRe = new Regex(@"<([^>]+)>");
        private static Regex _specialCharsRe = new Regex(@"[/!;]");

        private PhoneBookEntry(string phone, string address, string name)
        {
            Phone = phone;
            Address = address;
            Name = name;
        }

        public string Phone {get;}
        public string Address {get;}
        public string Name {get;}

        public static bool TryParse(string rawData, out PhoneBookEntry result)
        {
            result = null;
            var phoneMatch = _phoneRe.Match(rawData);
            if(phoneMatch.Success)
            {
                var nameMatch = _nameRe.Match(rawData);
                if(nameMatch.Success)
                {
                    var phone = phoneMatch.Groups[1].Value;
                    var name = nameMatch.Groups[1].Value;
                    var address = _specialCharsRe.Replace(_nameRe.Replace(_phoneRe.Replace(rawData, ""), ""), "").Trim();
                    result = new PhoneBookEntry(phone, address, name);
                    return true;
                }
            }

            return false;
        }

        public override string ToString()
        {
            return $"Phone => {Phone}, Name => {Name}, Address => {Address}";
        }            
    }
}

try it online

 

Object oriented solution in Perl. It also replaces an underscore by a space in the address as shown in the example. Space normalization was needed, too, as after removing the number or the name from the middle of the address, there can be spaces on both sides.

#!/usr/bin/perl
use warnings;
use strict;

{   package Phone::Book;
    sub new {
        my ($class, $file_handle) = @_;
        my %phonebook;
        while (<$file_handle>) {
            s/\+(\d{1,2}-\d{3}-\d{3}-\d{4})// and my $number = $1;
            s/<(.*?)>// and my $name = $1;
            my $address = s/^[^\w.]*|[^\w.]*$//gr;

            # This is not mentioned in the spec but appears in the examples.
            $address =~ s/_| {2,}/ /g;

            if (exists $phonebook{$number}) {
                $phonebook{$number} = {
                    Error => "Too many people: $number"};
            } else {
                $phonebook{$number} = {
                    Phone => $number, Name => $name, Address => $address};
            }
        }
        bless \%phonebook, $class
    }

    sub search {
        my ($self, $number) = @_;
        if (exists $self->{$number}) {
            return $self->{$number}
        } else {
            return {Error => "Not found: $number"}
        }
    }
}

use Test::More tests => 5;

my $pb = 'Phone::Book'->new(*DATA);

is_deeply $pb->search('00-000-000-0000'), {
    Error => 'Not found: 00-000-000-0000'};

is_deeply $pb->search('1-234-567-8901'), {
    Error => 'Too many people: 1-234-567-8901'};

is_deeply $pb->search('1-541-754-3010'), {
    Phone   => '1-541-754-3010',
    Name    => 'J Steeve',
    Address => '156 Alphand St.'};

is_deeply $pb->search('1-541-914-3010'), {
    Phone   => '1-541-914-3010',
    Name    => 'E Kustur',
    Address => '133, Green, Rd. NY-56423'};

is_deeply $pb->search('48-421-674-8974'), {
    Phone   => '48-421-674-8974',
    Name    => 'Anastasia',
    Address => 'Via Quirinal Roma'};

__DATA__
/+1-541-754-3010 156 Alphand_St. <J Steeve>
 133, Green, Rd. <E Kustur> NY-56423 ;+1-541-914-3010!
<Anastasia> +48-421-674-8974 Via Quirinal Roma
<Duplicate 1> +1-234-567-8901 Long Way, Tipperary
<Duplicate 2> 0, Invisible Path +1-234-567-8901