Introduction
Phone numbers as data: validate, inspect, search, generate.
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,
PhoneNumberobjects, or maps. -
Rich, keyword-keyed info maps with optional namespace inference (e.g. a key can be
:phone-number/typeor just:type; a type can be:phone-number.type/mobileor 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
PhoneNumberand reproducibility/debug data (including:phone-number.sample/random-seedand sampler stats). -
Specs with generators for generative testing, plus structured errors via
ex-infowith:phone-number/errorinex-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-norawon input data to parse numbers without preserving raw inputs. -
Use
phone-number.core/number-norawon 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
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).