Liking cljdoc? Tell your friends :D

Phone Number

Phone numbers as data: validate, inspect, search, generate.

Phone-number on Clojars Phone-number on cljdoc CircleCI

A pragmatic, data-oriented Clojure wrapper around Google's Libphonenumber for validating, inspecting, formatting, searching text, and generating phone numbers. It treats phone numbers as data: most functions accept strings, numbers, PhoneNumber instances, or keyword-keyed maps, and return values that are pleasant to pipe through.

Features

  • Data-first, polymorphic API: phone numbers can be expressed as numbers, strings, PhoneNumber objects, or maps.

  • Rich, keyword-keyed info maps with optional namespace inference (e.g. a key can be :phone-number/type or just :type; a type can be :phone-number.type/mobile or just :mobile).

  • Optional enrichment with carrier, location, and time zone data (via libphonenumber add-on modules), exposed through the same data-first API.

  • Stable options-map API for text searching (phone-number.core/find-numbers-opts), backed by lazy maps (LazyMap) so expensive info can be computed on demand.

  • Built-in support for short numbers (e.g. emergency numbers): validation, reachability, and expected cost class.

  • Sample generation that returns both the PhoneNumber and reproducibility/debug data (including :phone-number.sample/random-seed and sampler stats).

  • Specs with generators for generative testing, plus structured errors via ex-info with :phone-number/error in ex-data.

Caveats

If your program processes a lot of phone numbers and your strategy is to keep them in a native format (a result of calling phone-number.core/number), then be aware that, by default, all created PhoneNumber objects will have their raw input stored internally. This affects comparison in a way that the object representing a number +442920183133 will not be equal to the object representing the same number but with spaces (+44 2920 183 133). This is because the equality test is based, among other things, on raw input values, which are also used to generate each object's hash code.

To work around this, you have two options:

  • Use phone-number.core/number-noraw on input data to parse numbers without preserving raw inputs.

  • Use phone-number.core/number-noraw on existing objects to create raw-input-free copies.

Optionally, phone-number.core/number-optraw can also be used (especially in processing pipelines) to preserve raw input only when the created object is initialized from an existing one (an instance of PhoneNumber). For other argument types, the protocol method behaves like number-noraw.

Some functions validate their arguments and may throw clojure.lang.ExceptionInfo on invalid inputs. Such exceptions include :phone-number/error in ex-data, which can be used for error classification.

Sneak peeks

  • It can show information about phone numbers:
(require '[phone-number.core :as phone])

;; region taken from a phone number
;; using system's default locale

(phone/info "+44 29 2018 3133")

{:phone-number/country-code               44,
 :phone-number/dialing-region             :phone-number.region/gb,
 :phone-number.dialing-region/defaulted?  false,
 :phone-number.dialing-region/derived?    true,
 :phone-number.dialing-region/valid-for?  true,
 :phone-number/geographical?              true,
 :phone-number/location                   "Cardiff",
 :phone-number/possible?                  true,
 :phone-number/region                     :phone-number.region/gb,
 :phone-number/type                       :phone-number.type/fixed-line,
 :phone-number/valid?                     true,
 :phone-number.format/e164                "+442920183133",
 :phone-number.format/international       "+44 29 2018 3133",
 :phone-number.format/national            "029 2018 3133",
 :phone-number.format/raw-input           "+44 29 2018 3133",
 :phone-number.format/rfc3966             "tel:+44-29-2018-3133",
 :phone-number.tz-format/full-standalone  '("Greenwich Mean Time"),
 :phone-number.tz-format/id               '("Europe/London"),
 :phone-number.tz-format/short-standalone '("GMT"),
 :phone.number.short/possible?            false,
 :phone.number.short/valid?               false}

;; region passed as an argument
;; locale setting passed as an argument

(phone/info "601 100 601" :pl :pl)

{:phone-number/country-code               48,
 :phone-number/carrier                    "Plus",
 :phone-number/dialing-region             :phone-number.region/pl,
 :phone-number.dialing-region/defaulted?  false,
 :phone-number.dialing-region/derived?    true,
 :phone-number.dialing-region/valid-for?  true,
 :phone-number/geographical?              false,
 :phone-number/location                   "Polska",
 :phone-number/possible?                  true,
 :phone-number/region                     :phone-number.region/pl,
 :phone-number/type                       :phone-number.type/mobile,
 :phone-number/valid?                     true,
 :phone-number.format/e164                "+48601100601",
 :phone-number.format/international       "+48 601 100 601",
 :phone-number.format/national            "601 100 601",
 :phone-number.format/raw-input           "601 100 601",
 :phone-number.format/rfc3966             "tel:+48-601-100-601",
 :phone-number.tz-format/full-standalone  '("Czas środkowoeuropejski"),
 :phone-number.tz-format/id               '("Europe/Warsaw"),
 :phone-number.tz-format/short-standalone '("CET"),
 :phone.number.short/possible?            false,
 :phone.number.short/valid?               false}

(phone/info "8081 570001" :gb :en)

{:phone-number/country-code               44,
 :phone-number/dialing-region             :phone-number.region/gb,
 :phone-number.dialing-region/defaulted?  false,
 :phone-number.dialing-region/derived?    true,
 :phone-number.dialing-region/valid-for?  true,
 :phone-number/geographical?              false,
 :phone-number/possible?                  true,
 :phone-number/region                     :phone-number.region/gb,
 :phone-number/type                       :phone-number.type/toll-free,
 :phone-number/valid?                     true,
 :phone-number.format/e164                "+448081570001",
 :phone-number.format/international       "+44 808 157 0001",
 :phone-number.format/national            "0808 157 0001",
 :phone-number.format/raw-input           "8081 570001",
 :phone-number.format/rfc3966             "tel:+44-808-157-0001",
 :phone-number.tz-format/full-standalone  '("Greenwich Mean Time" "British Time"),
 :phone-number.tz-format/id               '("Europe/Guernsey"
                                            "Europe/Isle_of_Man"
                                            "Europe/Jersey"
                                            "Europe/London"),
 :phone-number.tz-format/short-standalone  '("GMT" "BT"),
 :phone.number.short/possible?             false,
 :phone.number.short/valid?                false}
  • It validates phone numbers:
(require '[phone-number.core :as phone])

(phone/valid? 8081570001 :gb)      ; => true
(phone/valid? "+448081570001")     ; => true
(phone/valid? 8081570001 :pl)      ; => false
(phone/valid? "8081570001")        ; => false

(phone/possible? "8081570001")     ; => false
(phone/possible? "8081570001" :gb) ; => true
(phone/possible? "8081570001" :pl) ; => true
  • It searches for phone numbers in text (recommended: the stable options-map API):
(require '[phone-number.core :as phone])

(->> (phone/find-numbers-opts
      "Call me at +44 808 157 0001!" {:region-code :gb
                                      :leniency    :valid
                                      :max-tries   1})
     first
     (into {}) ;; materialize lazy-map (also forces :phone-number/info)
     (dissoc :phone-number/number))

;; If you don't want the info map to be generated at all:
(->> (phone/find-numbers-opts
      "Call me at +44 808 157 0001!" {:region-code :gb
                                      :max-tries   1
                                      :locale-specification false})
     first
     (into {})
     (dissoc :phone-number/number))
  • It gives known phone number formats and types:
(require '[phone-number.core :as phone])

phone/formats

#{:phone-number.format/e164
  :phone-number.format/international
  :phone-number.format/national
  :phone-number.format/raw-input
  :phone-number.format/rfc3966}

phone/types

#{:phone-number.type/fixed-line
  :phone-number.type/fixed-line-or-mobile
  :phone-number.type/mobile
  :phone-number.type/pager
  :phone-number.type/personal
  :phone-number.type/premium-rate
  :phone-number.type/shared-cost
  :phone-number.type/toll-free
  :phone-number.type/uan
  :phone-number.type/unknown
  :phone-number.type/voicemail
  :phone-number.type/voip}
  • It generates phone numbers:
(phone/generate)

{:phone-number/info
    {:phone-number/country-code              213,
     :phone-number/geographical?             false,
     :phone-number/possible?                 true,
     :phone-number/region                    :phone-number.region/dz,
     :phone-number/type                      :phone-number.type/unknown,
     :phone-number/valid?                    false,
     :phone-number/dialing-region            :phone-number.region/dz,
     :phone-number.dialing-region/defaulted? false,
     :phone-number.dialing-region/derived?   true,
     :phone-number.dialing-region/valid-for? false,
     :phone-number.format/e164               "+213181525997",
     :phone-number.format/international      "+213 181525997",
     :phone-number.format/national           "181525997",
     :phone-number.format/rfc3966            "tel:+213-181525997",
     :phone.number.short/possible?           false,
     :phone.number.short/valid?              false},

 :phone-number/number #<com.google.i18n.phonenumbers.Phonenumber$PhoneNumber@3edea9e6>,
 :phone-number.sample/digits      ["+213" nil "181525997"],
 :phone-number.sample/hits        10,
 :phone-number.sample/max-samples 1000,
 :phone-number.sample/random-seed 7521527664400716800,
 :phone-number.sample/samples     11}

(require [phone-number.spec       :as spec]
         [clojure.spec.alpha      :as    s]
         [clojure.spec.gen.alpha  :as  gen])

(gen/generate (s/gen :phone-number/valid))

{:phone-number/info   #delay[{:status :pending, :val nil} 0x3810d15d],
 :phone-number/number #<com.google.i18n.phonenumbers.Phonenumber$PhoneNumber@79cc08fb>,
 :phone-number.sample/digits      ["+7" nil "937627908"],
 :phone-number.sample/hits        11,
 :phone-number.sample/max-samples 150,
 :phone-number.sample/random-seed 7581363778716192180,
 :phone-number.sample/samples     27}

(gen/generate (s/gen (s/and :phone-number/possible :phone-number/invalid)))

{:phone-number/info    #delay[{:status :pending, :val nil} 0x41c0e225],
 :phone-number/number #<com.google.i18n.phonenumbers.Phonenumber$PhoneNumber@36a74c18>,
 :phone-number.sample/digits      ["+84" nil "0270454"],
 :phone-number.sample/hits        8,
 :phone-number.sample/max-samples 200,
 :phone-number.sample/random-seed -9105741821593959780,
 :phone-number.sample/samples     12}

And more…

Installation

To use phone-number in your project, add the following to the dependencies section of project.clj or build.boot:

[io.randomseed/phone-number "3.23-0"]

For deps.edn, add the following as an element of a map under the :deps or :extra-deps key:

io.randomseed/phone-number {:mvn/version "3.23-0"}

Additionally, if you want to use the specs and generators provided by phone-number, add the following to your development profile:

org.clojure/spec.alpha {:mvn/version "0.6.249"}
org.clojure/test.check {:mvn/version "1.1.3"}

You can also download the JAR from Clojars.

Documentation

Full documentation including usage examples is available at:

License

Copyright © 2020–2026 Paweł Wilk

Phone-number is copyrighted software owned by Paweł Wilk (pw@gnu.org). You may redistribute and/or modify this software as long as you comply with the terms of the GNU Lesser General Public License (version 3).

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Development

CircleCI

Building docs

make docs

Building JAR

make jar

Rebuilding POM

make pom

Signing POM

make sig

Deploying to Clojars

make deploy

Interactive development

bin/repl

Starts a REPL and an nREPL server (the port is stored in .nrepl-port).

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close