randomseed-io/phone-number
Phone numbers as data: validate, inspect, search, generate.
Phone Number
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-info
with: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 "9.0.23-4"]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 "9.0.23-4"}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 docsBuilding JAR
make jarRebuilding POM
make pomSigning POM
make sigDeploying to Clojars
make deployInteractive development
bin/replStarts a REPL and an nREPL server (the port is stored in .nrepl-port).