pico_phone 0.8.4-aarch64-linux → 0.8.5-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +561 -0
  3. data/lib/pico_phone/version.rb +1 -1
  4. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 56d1043ada1863b566650d323e3f683941149edb62e531b966080cb3c186ad65
4
- data.tar.gz: 569f732f6bcccf0c8f80d40bc63e982da7eebaaf1da1356bb412ae20657f6e92
3
+ metadata.gz: b7c595f4f55a3be9f1cfb723ec5114fb98bd71a651ffadc7558faf35070d0523
4
+ data.tar.gz: 0e3b33720bce4842e22bfabcfecc10df3fb199c0023143541b558d3d7fea8508
5
5
  SHA512:
6
- metadata.gz: e0d24e5746ec066fff98c342cbb6cd92cbe0710bd07a0d630c5029035b16364b023956d63292c8fdbe5d133e5bfe2acc942d159edcf2679c0d03edd3ceea4eea
7
- data.tar.gz: 8f70672482f99ce9d2ba46e1d2d44368436be28544c24ac13109450ea395ed2b6f8328f23e6bdb728ec2510f95f19332dbbf1ca9529039df13bf1cf8fa4b8a8c
6
+ metadata.gz: 0bfa31b236a879ea543b6376c6cd805ad227ba0cac86806cf65cc4fee741ce307510cdf6514bf5b57f6b02660a9a529257c4500ebcd65a0bee6299261c483f04
7
+ data.tar.gz: 5d421a51557c6ced42063e9d87e52930fecb93984209fd0825c8cb936793145fb1992ab04949a8d84f8d242e189fd3c0906071584aefde10845708db3c3481d8
data/README.md ADDED
@@ -0,0 +1,561 @@
1
+ # PicoPhone
2
+
3
+ [![CI](https://github.com/gjack/pico_phone/actions/workflows/ci.yml/badge.svg)](https://github.com/gjack/pico_phone/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/pico_phone.svg?v=1)](https://badge.fury.io/rb/pico_phone)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ A thin Ruby wrapper around Google's [libphonenumber](https://github.com/google/libphonenumber) C++ library, built with [Rice](https://github.com/ruby-rice/rice). It uses the same engine as Android's dialer for parsing, validating, and formatting phone numbers in any country.
8
+
9
+ ## Installation
10
+
11
+ Add to your Gemfile:
12
+
13
+ ```ruby
14
+ gem 'pico_phone'
15
+ ```
16
+
17
+ Or install directly:
18
+
19
+ ```
20
+ gem install pico_phone
21
+ ```
22
+
23
+ Pre-compiled native gems are available for `arm64-darwin` (Apple Silicon Macs), `x86_64-linux` and `aarch64-linux` (glibc 2.34 or newer: Debian 12+, Ubuntu 22.04+, and similar), and `x86_64-linux-musl` and `aarch64-linux-musl` (Alpine and similar), for Ruby 3.1–3.4 and 4.0. On these platforms and Ruby versions, no system libraries or compiler are required — Bundler will select the right binary automatically.
24
+
25
+ RubyGems and Bundler pick a gem by CPU, OS and libc name only, so on a glibc distro older than the versions above (for example RHEL/Alma/Rocky 9 or Amazon Linux 2023) they will still select the precompiled gem, which then fails to load. Force the source build there instead — `gem install pico_phone --platform ruby`, or `bundle config set force_ruby_platform true` for Bundler — after installing libphonenumber as described below.
26
+
27
+ On other platforms the gem compiles from source and requires libphonenumber:
28
+
29
+ ```
30
+ # macOS
31
+ brew install libphonenumber
32
+
33
+ # Ubuntu/Debian
34
+ sudo apt-get install libphonenumber-dev libicu-dev
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Checking if a phone number is valid
40
+
41
+ ```
42
+ PicoPhone.valid?("+15102745656") #true
43
+ PicoPhone.valid_for_country?("(510) 274-5556", "BR") #false
44
+
45
+ phone = PicoPhone.parse("(510) 274-5556", "BR")
46
+ phone.valid? #false
47
+ ```
48
+
49
+ ### Checking if a phone number is possible
50
+
51
+ ```
52
+ PicoPhone.possible?("+15102745556") #true
53
+ PicoPhone.possible?("It's my number") #false
54
+
55
+ phone = PicoPhone.parse("-245")
56
+ phone.possible? #false
57
+ ```
58
+
59
+ `possible_with_reason` returns a symbol explaining why a number is or is not possible, which is more useful than a boolean when generating validation messages.
60
+
61
+ ```
62
+ phone = PicoPhone.parse("+15102745656", "US")
63
+ phone.possible_with_reason # :is_possible
64
+
65
+ phone = PicoPhone.parse("123", "US")
66
+ phone.possible_with_reason # :too_short
67
+
68
+ phone = PicoPhone.parse("123456789000", "US")
69
+ phone.possible_with_reason # :too_long
70
+ ```
71
+
72
+ Possible return values are `:is_possible`, `:is_possible_local_only`, `:too_short`, `:too_long`, `:invalid_country_code`, and `:invalid_length`.
73
+
74
+ ### Formatting a phone number
75
+
76
+ ```
77
+ phone = PicoPhone.parse("5102745656", "US")
78
+
79
+ phone.national # (510) 274-5656
80
+ phone.international # +1 510-274-5656
81
+ phone.e164 # +15102745656
82
+ phone.raw_national # 5102745656
83
+ phone.raw_international # 15102745656
84
+
85
+ phone = PicoPhone.parse("0435582008", "AU")
86
+
87
+ phone.national # 0435 582 008
88
+ phone.international # +61 435 582 008
89
+ phone.e164 # +61435582008
90
+ phone.raw_national # 435582008
91
+ phone.raw_international # 61435582008
92
+ ```
93
+
94
+ ### Country codes, area codes, and local number
95
+
96
+ ```
97
+ phone = PicoPhone.parse("5102745656", "US")
98
+
99
+ phone.country_code # 1
100
+ phone.area_code # "510"
101
+ phone.local_number # "2745656"
102
+
103
+ phone = PicoPhone.parse("0435582008", "AU")
104
+
105
+ phone.country_code # 61
106
+ phone.area_code # "" (AU mobile numbers have no geographical area code)
107
+ phone.local_number # "435582008" (full national number when there is no area code)
108
+ ```
109
+
110
+ `country_calling_code` returns the international calling code for a region code, without needing to parse a number first. Returns `0` for unknown or invalid region codes.
111
+
112
+ ```
113
+ PicoPhone.country_calling_code("US") # 1
114
+ PicoPhone.country_calling_code("FR") # 33
115
+ PicoPhone.country_calling_code("BR") # 55
116
+ PicoPhone.country_calling_code("CA") # 1 (NANP — same calling code as US)
117
+ PicoPhone.country_calling_code("XX") # 0 (unknown region)
118
+ ```
119
+
120
+ ### Truncating too-long numbers
121
+
122
+ `truncate` removes trailing digits from a number that is too long until a valid number is found. Returns a new `PhoneNumber` on success, or `nil` when the number was already valid or no valid truncation exists. The receiver is never mutated.
123
+
124
+ ```
125
+ phone = PicoPhone.parse("+151027456560000", "US")
126
+ phone.valid? # false
127
+
128
+ truncated = phone.truncate
129
+ truncated.e164 # "+15102745656"
130
+ truncated.valid? # true
131
+
132
+ PicoPhone.parse("+15102745656").truncate # nil (already valid)
133
+ PicoPhone.parse("garbage").truncate # nil (cannot be truncated)
134
+ ```
135
+
136
+ ### Geographic description
137
+
138
+ ```
139
+ phone = PicoPhone.parse("5102745656", "US")
140
+
141
+ phone.geo_name # "California"
142
+ phone.geo_name("de") # description in another language, where available
143
+
144
+ phone = PicoPhone.parse("0435582008", "AU")
145
+
146
+ phone.geo_name # "Australia" (falls back to the country name when no finer description exists)
147
+ ```
148
+
149
+ Backed by libphonenumber's own offline geocoder, not a separate re-implementation — it loads its area-code data lazily, one region/language file at a time, so looking up a number doesn't pull the whole world's geocoding data into memory (see the memory benchmark in [Benchmarks](#benchmarks)).
150
+
151
+ ### Carrier lookup
152
+
153
+ ```
154
+ phone = PicoPhone.parse("6001234567", "IN")
155
+
156
+ phone.carrier_name # "Reliance Jio"
157
+ phone.carrier_name("de") # carrier name in another language, where available
158
+
159
+ phone = PicoPhone.parse("5102745656", "US")
160
+
161
+ phone.carrier_name # "" (no carrier mapping for this prefix -- number portability makes carrier-by-prefix unreliable in many countries, including the US)
162
+ ```
163
+
164
+ `carrier_name` returns the carrier the number was **originally allocated to**, not necessarily its current carrier — in countries that support mobile number portability, a number may have since been ported away from that carrier. This matches libphonenumber's own Java carrier mapper, which documents the same caveat verbatim ("the carrier name is the one the number was originally allocated to, however if the country supports mobile number portability the number might not belong to the returned carrier anymore").
165
+
166
+ libphonenumber has no upstream C++ port of its carrier mapper (only Java), so this is a first-party pico_phone implementation reusing the same lazy per-(country code, language) loading machinery as `geo_name` above, rather than a Ruby-level reimplementation. Unlike `geo_name`, there's no fallback to a country-level description when a prefix has no carrier data — an unmapped number just returns an empty string, matching phonelib's `carrier` semantics.
167
+
168
+ ### Timezone lookup
169
+
170
+ ```
171
+ phone = PicoPhone.parse("+12082123456")
172
+
173
+ phone.timezones # ["America/Boise", "America/Los_Angeles"]
174
+
175
+ phone = PicoPhone.parse("6001234567", "IN")
176
+
177
+ phone.timezones # ["Asia/Calcutta"]
178
+
179
+ phone = PicoPhone.parse("+33612345678")
180
+
181
+ phone.timezones # ["Europe/Paris"]
182
+ ```
183
+
184
+ `timezones` returns an array of [IANA time zone identifiers](https://www.iana.org/time-zones) for the number's prefix. A number can map to more than one timezone when its prefix covers a geographic area that spans multiple zones — NANP area codes in particular span many US time zones. The method returns an empty array when no mapping exists for the number's prefix.
185
+
186
+ libphonenumber has no upstream C++ port of its timezone mapper (only Java), so this is a first-party pico_phone implementation using the same prefix-lookup machinery (`AreaCodeMap`) as `geo_name` and `carrier_name`. The timezone table is small enough (~86KB on disk) to load once at first call rather than lazily per-region — the initial load cost is negligible.
187
+
188
+ ### Checking validity for a specific country on a parsed number
189
+
190
+ The module-level `valid_for_country?` accepts a raw string. The same check is also available as an instance method once a number has been parsed.
191
+
192
+ ```
193
+ phone = PicoPhone.parse("+15102745656")
194
+
195
+ phone.valid_for_country?("US") # true
196
+ phone.valid_for_country?("AU") # false
197
+ phone.invalid_for_country?("US") # false
198
+ phone.invalid_for_country?("AU") # true
199
+ ```
200
+
201
+ ### Original input and string conversion
202
+
203
+ `original` returns the input exactly as it was passed to `parse`, regardless of whether parsing succeeded. `to_s` returns the e164 format for a valid number, and falls back to the original input for an invalid one.
204
+
205
+ ```
206
+ phone = PicoPhone.parse("5102745656", "US")
207
+ phone.original # "5102745656"
208
+ phone.to_s # "+15102745656"
209
+
210
+ phone = PicoPhone.parse("not a number")
211
+ phone.original # "not a number"
212
+ phone.to_s # "not a number"
213
+ ```
214
+
215
+ ### Additional formatting options
216
+
217
+ `format_in_original_format` returns the number formatted in the same style it was originally entered — international if a `+` prefix was used, national otherwise.
218
+
219
+ ```
220
+ PicoPhone.parse("+15102745656", "US").format_in_original_format # "+1 510-274-5656"
221
+ PicoPhone.parse("5102745656", "US").format_in_original_format # "(510) 274-5656"
222
+ ```
223
+
224
+ `out_of_country_format(region)` returns the dialing string needed to reach the number from a given country. Within shared-prefix regions such as NANP no international prefix is prepended; from other regions the appropriate dialing prefix for the calling country is used.
225
+
226
+ ```
227
+ phone = PicoPhone.parse("+15102745656", "US")
228
+
229
+ phone.out_of_country_format("US") # "1 (510) 274-5656"
230
+ phone.out_of_country_format("GB") # "00 1 510-274-5656"
231
+ phone.out_of_country_format("AU") # "0011 1 510-274-5656"
232
+ ```
233
+
234
+ `mobile_dialing_format(region)` returns the most convenient representation for dialing the number from a mobile device in the given region, typically the full international format with separators.
235
+
236
+ ```
237
+ phone = PicoPhone.parse("+15102745656", "US")
238
+
239
+ phone.mobile_dialing_format("US") # "+1 510-274-5656"
240
+ phone.mobile_dialing_format("GB") # "+1 510-274-5656"
241
+ ```
242
+
243
+ ### Comparing phone numbers
244
+
245
+ `number_match` compares two phone number strings and returns how closely they match. E.164 input gives the most precise result; national-format strings without a country code may return `:nsn_match` where a full E.164 comparison would return `:exact_match`.
246
+
247
+ ```
248
+ PicoPhone.number_match("+15102745656", "+15102745656") # :exact_match
249
+ PicoPhone.number_match("+15102745656", "5102745656") # :nsn_match (no country code in second arg)
250
+ PicoPhone.number_match("+15102745656", "2745656") # :short_nsn_match
251
+ PicoPhone.number_match("+15102745656", "+61435582008") # :no_match
252
+ PicoPhone.number_match("+15102745656", "garbage") # :invalid_number
253
+ ```
254
+
255
+ `match_type` is the instance-method equivalent. It accepts either a string or a `PhoneNumber`. When passed a `PhoneNumber`, the country code already embedded in the parsed object is used, which gives `:exact_match` where a bare national-format string would give `:nsn_match`.
256
+
257
+ ```
258
+ phone = PicoPhone.parse("+15102745656")
259
+
260
+ phone.match_type("+15102745656") # :exact_match
261
+ phone.match_type(PicoPhone.parse("5102745656", "US")) # :exact_match (country code resolved at parse time)
262
+ phone.match_type("5102745656") # :nsn_match (string has no country code)
263
+ phone.match_type("+61435582008") # :no_match
264
+ ```
265
+
266
+ Possible return values are `:exact_match`, `:nsn_match`, `:short_nsn_match`, `:no_match`, and `:invalid_number`.
267
+
268
+ ### Finding phone numbers in text
269
+
270
+ `find_numbers` scans an arbitrary block of text and returns every phone number found, along with its position and the raw substring that was matched. It uses libphonenumber's `PhoneNumberMatcher` internally.
271
+
272
+ ```
273
+ matches = PicoPhone.find_numbers("Call me at +1 425 882-8080 or (650) 253-0000 for details.", "US")
274
+
275
+ matches.size # 2
276
+ matches.first.raw_string # "+1 425 882-8080"
277
+ matches.first.start # 11
278
+ matches.first.end_index # 26
279
+ matches.first.number # #<PicoPhone::PhoneNumber>
280
+ matches.first.number.e164 # "+14258828080"
281
+ ```
282
+
283
+ Each element in the returned array is a `PicoPhone::PhoneNumberMatch` with four methods:
284
+
285
+ | Method | Returns |
286
+ |--------|---------|
287
+ | `start` | Byte offset of the match start in the searched text |
288
+ | `end_index` | Exclusive byte offset of the match end (`text[start...end_index]` is the match) |
289
+ | `raw_string` | The matched substring exactly as it appears in the text |
290
+ | `number` | A fully-parsed `PicoPhone::PhoneNumber` |
291
+
292
+ The `leniency` keyword argument controls how strictly a candidate is required to look like a phone number before being included. Defaults to `:valid`.
293
+
294
+ ```
295
+ # :possible — accept any number-like sequence with a plausible digit count
296
+ PicoPhone.find_numbers(text, "US", leniency: :possible)
297
+
298
+ # :valid (default) — must be a valid number for its region
299
+ PicoPhone.find_numbers(text, "US", leniency: :valid)
300
+
301
+ # :strict_grouping — must also be formatted in a regionally plausible grouping
302
+ PicoPhone.find_numbers(text, "US", leniency: :strict_grouping)
303
+
304
+ # :exact_grouping — must match exactly how libphonenumber would format it
305
+ PicoPhone.find_numbers(text, "US", leniency: :exact_grouping)
306
+ ```
307
+
308
+ > **Note:** Some Linux distributions ship a `libphonenumber` system package compiled with
309
+ > `USE_ALTERNATE_FORMATS=OFF`, which disables alternate-format pattern recognition used by
310
+ > `find_numbers`. If the method returns fewer matches than expected on Linux, building with
311
+ > `PICO_PHONE_NATIVE_BUILD=1` uses the vendored library, which always enables alternate formats.
312
+
313
+ ### Finding possible or valid countries for a phone number
314
+
315
+ A calling code is not always 1:1 with a country. `+1` covers the US, Canada, and around 20 Caribbean territories. `+7` covers both Russia and Kazakhstan. These methods let you find which countries a given number could belong to.
316
+
317
+ `possible_countries` uses a lenient check: for unambiguous calling codes (e.g. `+33` for France) it only requires the number to be the right length. For ambiguous calling codes it narrows the list down using full pattern validation, since length alone cannot distinguish between candidates.
318
+
319
+ `valid_countries` always applies full pattern validation and returns only the regions where the number is genuinely valid.
320
+
321
+ ```
322
+ PicoPhone.possible_countries("+15102745656") # ["US"]
323
+ PicoPhone.possible_countries("+12423570000") # ["BS"] (Bahamas, also a NANP number)
324
+ PicoPhone.possible_countries("+78005553535") # ["RU", "KZ"]
325
+
326
+ PicoPhone.valid_countries("+15102745656") # ["US"]
327
+ PicoPhone.valid_countries("+78005553535") # ["RU", "KZ"]
328
+
329
+ # A number that is the right length for France but fails pattern validation
330
+ PicoPhone.possible_countries("+33000000000") # ["FR"]
331
+ PicoPhone.valid_countries("+33000000000") # []
332
+
333
+ # Both methods return [] for unparseable input
334
+ PicoPhone.possible_countries("not a number") # []
335
+
336
+ phone = PicoPhone.parse("+15102745656")
337
+ phone.possible_countries # ["US"]
338
+ phone.valid_countries # ["US"]
339
+ ```
340
+
341
+ ### Number characteristics
342
+
343
+ `geographical?` returns true for fixed-line numbers tied to a geographic area code. Mobile, toll-free, and other non-geographic number types return false.
344
+
345
+ ```
346
+ PicoPhone.parse("+15102745656", "US").geographical? # true (fixed-line with area code)
347
+ PicoPhone.parse("+61435582008", "AU").geographical? # false (AU mobile)
348
+ PicoPhone.parse("+18005551234", "US").geographical? # false (toll-free)
349
+ ```
350
+
351
+ `can_be_internationally_dialled?` returns false for numbers that can only be reached from within their own country.
352
+
353
+ ```
354
+ PicoPhone.parse("+15102745656", "US").can_be_internationally_dialled? # true
355
+ ```
356
+
357
+ ### Supported types for a region
358
+
359
+ `supported_types_for_region` returns the phone number types that actually exist in a given country. The set of types varies significantly between regions.
360
+
361
+ ```
362
+ PicoPhone.supported_types_for_region("US")
363
+ # => [:fixed_line, :mobile, :toll_free, :premium_rate, :personal_number]
364
+
365
+ PicoPhone.supported_types_for_region("AU")
366
+ # => [:fixed_line, :mobile, :toll_free, :premium_rate, :shared_cost, :voip, :pager]
367
+ ```
368
+
369
+ ### Example numbers
370
+
371
+ `example_number` returns a valid example `PhoneNumber` instance for a region. `example_number_for_type` returns an example for a specific number type. Both return a fully parsed object, so all the usual methods are available on the result.
372
+
373
+ ```
374
+ PicoPhone.example_number("AU").e164 # "+61212345678"
375
+ PicoPhone.example_number("AU").national # "02 1234 5678"
376
+
377
+ PicoPhone.example_number_for_type("US", :toll_free).e164 # "+18002345678"
378
+ PicoPhone.example_number_for_type("AU", :mobile).e164 # "+61412345678"
379
+ ```
380
+
381
+ > **Note:** Some Linux distributions ship a `libphonenumber` system package compiled with
382
+ > `USE_LITE_METADATA=ON`, which strips example number data from the library. If
383
+ > `example_number` or `example_number_for_type` returns `nil` on Linux, this is the cause.
384
+ > Build with `PICO_PHONE_NATIVE_BUILD=1` to use the vendored library, which always includes
385
+ > full metadata.
386
+
387
+ ### Checking if a number is possible for a specific type
388
+
389
+ `possible_for_type?` checks whether a number's digit count is consistent with a given type in its region. This is a length-based check — more permissive than `type`, which classifies the number strictly.
390
+
391
+ ```
392
+ phone = PicoPhone.parse("+15102745656", "US")
393
+ phone.possible_for_type?(:fixed_line_or_mobile) # true
394
+ phone.possible_for_type?(:toll_free) # true (same digit count)
395
+
396
+ phone.type # :fixed_line_or_mobile (strict classification)
397
+ ```
398
+
399
+ ### Vanity numbers
400
+
401
+ `alpha_number?` identifies vanity number strings before parsing or converting them. `convert_alpha_characters` converts the alpha characters to their dialable digit equivalents.
402
+
403
+ ```
404
+ PicoPhone.alpha_number?("1-800-FLOWERS") # true
405
+ PicoPhone.alpha_number?("+15102745656") # false
406
+
407
+ PicoPhone.convert_alpha_characters("1-800-FLOWERS") # "1-800-3569377"
408
+ ```
409
+
410
+ Once converted, the result can be passed to `parse` as usual.
411
+
412
+ ```
413
+ PicoPhone.parse(PicoPhone.convert_alpha_characters("1-800-FLOWERS")).type # :toll_free
414
+ ```
415
+
416
+ ### Supported regions
417
+
418
+ `supported_regions` returns an array of all region codes the library knows about. Useful for populating a country selector or validating a region code before passing it elsewhere.
419
+
420
+ ```
421
+ PicoPhone.supported_regions # ["AC", "AD", "AE", ...]
422
+ PicoPhone.supported_regions.size # 245
423
+ PicoPhone.supported_regions.include?("US") # true
424
+ ```
425
+
426
+ ### Short numbers and emergency services
427
+
428
+ Short numbers (emergency services, short codes, etc.) cannot be parsed as regular phone numbers and require a region to be meaningful. These methods wrap libphonenumber's `ShortNumberInfo` API.
429
+
430
+ `emergency_number?` checks whether a string exactly matches an emergency service number in the given region.
431
+
432
+ ```
433
+ PicoPhone.emergency_number?("911", "US") # true
434
+ PicoPhone.emergency_number?("999", "GB") # true
435
+ PicoPhone.emergency_number?("411", "US") # false
436
+ ```
437
+
438
+ `short_number_valid?` checks whether a string is a valid short number in the given region.
439
+
440
+ ```
441
+ PicoPhone.short_number_valid?("911", "US") # true
442
+ PicoPhone.short_number_valid?("411", "US") # true
443
+ PicoPhone.short_number_valid?("411", "GB") # false
444
+ ```
445
+
446
+ `short_number_cost` returns the cost category of a short number as a symbol. Emergency numbers are always `:toll_free`. Returns `:unknown_cost` if the number cannot be parsed or has no cost data for the region.
447
+
448
+ ```
449
+ PicoPhone.short_number_cost("911", "US") # :toll_free
450
+ PicoPhone.short_number_cost("411", "US") # :unknown_cost
451
+ ```
452
+
453
+ Possible return values are `:toll_free`, `:standard_rate`, `:premium_rate`, and `:unknown_cost`.
454
+
455
+ ### Extensions
456
+
457
+ PicoPhone exposes libphonenumber's methods that identify and extract the extension out of a parsed phone number.
458
+
459
+ ```
460
+ phone = PicoPhone.parse("5102745656;456", "US")
461
+
462
+ phone.has_extension? # true
463
+ phone.extension # 456
464
+ ```
465
+
466
+ You can also format a phone number including its extension. The extension prefix can also be customized as needed. If no custom value is indicated, it will default to `;`
467
+
468
+ ```
469
+ PicoPhone.default_extension_prefix = " ext. "
470
+
471
+ phone = PicoPhone.parse("5102745656;456", "US")
472
+
473
+ phone.full_national # (510) 274-5656 ext. 456
474
+ ```
475
+
476
+
477
+ ## Benchmarks
478
+
479
+ pico_phone wraps the actual libphonenumber C++ engine via [Rice](https://github.com/ruby-rice/rice). [phonelib](https://github.com/daddyz/phonelib) instead reimplements libphonenumber's pattern-matching in pure Ruby. That difference in architecture shows up clearly in throughput, and less clearly (and more conditionally) in memory.
480
+
481
+ The numbers below are one data point — macOS arm64, Ruby 3.4.1, pico_phone vs. phonelib 0.10.22 — not a guarantee for your platform. Run `bundle exec rake bench` yourself to reproduce them (requires `bundle install`, which pulls in `phonelib` and `benchmark-ips` as dev-only dependencies for comparison).
482
+
483
+ ### Speed
484
+
485
+ ```
486
+ $ bundle exec rake bench:speed
487
+
488
+ pico_phone phonelib
489
+ valid? 128.1k i/s 35.9k i/s (4.7x slower)
490
+ parse + e164 168.8k i/s 64.9k i/s (2.6x slower)
491
+ parse + national 109.5k i/s 47.4k i/s (3.6x slower)
492
+ valid? (30 numbers, multi-region) 3.0k i/s 1.0k i/s (3.0x slower)
493
+ carrier_name / carrier 173.5k i/s 42.9k i/s (4.1x slower)
494
+ timezones / timezone 98.1k i/s 2.5k i/s (39.7x slower)
495
+ ```
496
+
497
+ Calling into compiled C++ beats matching regexes in Ruby, as expected.
498
+
499
+ ### Memory
500
+
501
+ ```
502
+ $ bundle exec rake bench:memory
503
+
504
+ Scenario Peak MB
505
+ ---------------------------------------------------
506
+ baseline 21.4
507
+ pico_phone (light) 29.8
508
+ pico_phone (heavy) 40.7
509
+ pico_phone (heavy, +geo_name) 39.9
510
+ pico_phone (heavy, +carrier_name) 39.7
511
+ pico_phone (heavy, +timezones) 41.1
512
+ phonelib (light) 24.2
513
+ phonelib (heavy) 31.2
514
+ phonelib (heavy, +geo/carrier/timezone) 184.6
515
+ ```
516
+
517
+ For validate/parse/format, memory use is comparable between the two, and pico_phone isn't the clear winner: it uses somewhat *more* than phonelib in this test (partly the cost of linking in libphonenumber's offline geocoder and carrier mapper, used below). Memory stays flat under sustained load for both (no leak).
518
+
519
+ phonelib's footprint balloons specifically when something calls `geo_name`, `carrier`, or `timezone`. Those lazily `Marshal.load` a ~4MB serialized data file — area-code-to-city, carrier, and timezone tables for every region in the world — into nested Ruby `Hash`/`Array`/`String` objects, then cache the whole thing in a class variable for the life of the process (see `phonelib/core.rb`, `@@phone_ext_data`).
520
+
521
+ pico_phone's `geo_name` (backed by libphonenumber's C++ offline geocoder, see [Geographic description](#geographic-description)) and `carrier_name` (see [Carrier lookup](#carrier-lookup)) don't have this problem: both load their prefix data lazily, one (country code, language) file at a time, and cache only what's actually queried — calling either 600k times across 30 regions above added no measurable RSS over the no-lookup baseline (both landed at or below the plain `pico_phone (heavy)` figure, within run-to-run noise). `timezones` (see [Timezone lookup](#timezone-lookup)) loads its single flat table (~86KB) once at first call and caches it for the life of the process — the one-time cost is negligible given the small table size.
522
+
523
+ ## Development
524
+
525
+ After checking out the repo, run `bin/setup` to install dependencies. Running `rake spec` will run the tests. If you make any changes to the `pico_phone.cpp` file, running `rake` will compile the gem with the new changes and run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
526
+
527
+ ### Cross-platform verification
528
+
529
+ The extension links against libphonenumber, which behaves differently across platforms (system library versions, linking strategies, Abseil ABI). Before merging changes to the C++ extension, verify across all supported platforms:
530
+
531
+ ```bash
532
+ # macOS arm64 — dynamic (default developer workflow)
533
+ bundle exec rake
534
+
535
+ # macOS arm64 — static (the NATIVE_BUILD path used for precompiled gems)
536
+ PICO_PHONE_NATIVE_BUILD=1 bundle exec rake compile spec
537
+ ```
538
+
539
+ For Linux, use `verify_docker.sh` (requires Docker Desktop):
540
+
541
+ ```bash
542
+ bash verify_docker.sh dynamic # Ubuntu arm64 + x86_64, dynamic linking (~2 min each)
543
+ bash verify_docker.sh static # Ubuntu arm64 + x86_64, NATIVE_BUILD=1 (~8–15 min each)
544
+ bash verify_docker.sh musl # Alpine arm64 + x86_64, always NATIVE_BUILD=1 (~10–15 min each)
545
+ bash verify_docker.sh portable # Debian 12 arm64 + x86_64: the glibc release recipe, plus a smoke test of the packaged gem on bare Debian images
546
+ bash verify_docker.sh # all eight Linux combinations
547
+ ```
548
+
549
+ The static Linux runs compile abseil, protobuf, ICU, and libphonenumber from source inside the container — that's what makes them slow. The dynamic runs use Ubuntu's packaged `libphonenumber-dev` and are much faster. Alpine has no equivalent system package, so musl is always static.
550
+
551
+ The `portable` runs are the ones that match how the glibc release gems are built: inside a Debian 12 container, so the resulting binary only needs glibc 2.34 / libstdc++ 3.4.30 (the versions `verify_docker.sh` asserts as its floor; keep them in sync with the platform line in [Installation](#installation)). They then install the packaged gem into bare `ruby:slim` images with no ICU and no compiler and exercise it (`smoke_packaged_gem.sh`), because a passing spec suite on the build machine can't tell you whether the shipped artifact loads elsewhere.
552
+
553
+ ## Contributing
554
+
555
+ Bug reports and pull requests are welcome on GitHub at https://github.com/gjack/pico_phone.
556
+
557
+ ## License
558
+
559
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
560
+
561
+ pico_phone's precompiled native gems compile against and statically embed several third-party libraries — libphonenumber and Abseil (Apache License 2.0), protobuf (BSD-3-Clause), ICU (Unicode License v3), and, on macOS, Boost (Boost Software License 1.0) — plus Rice (BSD-2-Clause-style), which is compiled directly in as a header-only library. Their licenses are reproduced in full in [`THIRD_PARTY_LICENSES.txt`](THIRD_PARTY_LICENSES.txt), which ships with the gem.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PicoPhone
4
- VERSION = "0.8.4"
4
+ VERSION = "0.8.5"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pico_phone
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.4
4
+ version: 0.8.5
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Gabi Jack
@@ -77,6 +77,7 @@ extensions: []
77
77
  extra_rdoc_files: []
78
78
  files:
79
79
  - LICENSE.txt
80
+ - README.md
80
81
  - THIRD_PARTY_LICENSES.txt
81
82
  - ext/pico_phone/carrier_data.cc
82
83
  - ext/pico_phone/carrier_mapper.cc