phony 1.6.5 → 1.6.6

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.
data/lib/phony.rb CHANGED
@@ -12,6 +12,8 @@ require File.expand_path '../phony/national_splitters/default', __FILE__
12
12
  require File.expand_path '../phony/national_code', __FILE__
13
13
  require File.expand_path '../phony/country', __FILE__
14
14
  require File.expand_path '../phony/country_codes', __FILE__
15
+ require File.expand_path '../phony/validator', __FILE__
16
+ require File.expand_path '../phony/validators', __FILE__
15
17
  require File.expand_path '../phony/dsl', __FILE__
16
18
 
17
19
  # Countries.
@@ -37,7 +39,8 @@ module Phony
37
39
 
38
40
  # Phony uses a single country codes instance.
39
41
  #
40
- @codes = CountryCodes.instance
42
+ @codes = CountryCodes.instance
43
+ @validator = Validators.instance
41
44
 
42
45
  class << self
43
46
 
@@ -74,6 +77,16 @@ module Phony
74
77
  end
75
78
  alias formatted format
76
79
  alias formatted! format!
80
+
81
+ # Makes a plausibility check.
82
+ #
83
+ # If it returns false, it is not plausible.
84
+ # If it returns true, it is unclear whether it is plausible,
85
+ # leaning towards being plausible.
86
+ #
87
+ def plausible? number, hints = {}
88
+ @validator.plausible? number, hints
89
+ end
77
90
 
78
91
  # def service? number
79
92
  # @codes.service? number.dup
@@ -27,7 +27,9 @@ Phony.define do
27
27
 
28
28
  # USA, Canada, etc.
29
29
  #
30
- country '1', fixed(3) >> split(3,4)
30
+ country '1',
31
+ fixed(3) >> split(3,4),
32
+ invalid_ndcs(/911/)
31
33
 
32
34
  # Kazakhstan (Republic of) & Russian Federation.
33
35
  #
@@ -64,31 +66,33 @@ Phony.define do
64
66
 
65
67
  # Spain.
66
68
  #
67
- country '34', fixed(2) >> split(3,4)
69
+ country '34',
70
+ fixed(2) >> split(3,4)
68
71
 
69
72
  # Hungary.
70
73
  #
71
74
  # TODO Mobile.
72
75
  #
73
- country '36', one_of('104','105','107','112') >> split(3,3) | # Service
74
- one_of('1') >> split(3,4) | # Budapest
75
- fixed(2) >> split(3,4) # 2-digit NDCs
76
+ country '36',
77
+ one_of('104','105','107','112') >> split(3,3) | # Service
78
+ one_of('1') >> split(3,4) | # Budapest
79
+ fixed(2) >> split(3,4) # 2-digit NDCs
76
80
 
77
81
  # country '39' # Italy, see special file.
78
82
 
79
83
  # Romania.
80
84
  #
81
- country '40', match(/^(112|800|90[036])\d+$/) >> split(3,3) | # Service
82
- match(/^(7[1-8])\d+$/) >> split(3,4) | # Mobile
83
- one_of('21', '31') >> split(3,4) | # Bucureşti
84
- fixed(3) >> split(3,4) # 3-digit NDCs
85
+ country '40',
86
+ match(/^(112|800|90[036])\d+$/) >> split(3,3) | # Service
87
+ match(/^(7[1-8])\d+$/) >> split(3,4) | # Mobile
88
+ one_of('21', '31') >> split(3,4) | # Bucureşti
89
+ fixed(3) >> split(3,4) # 3-digit NDCs
85
90
 
86
91
  # Switzerland.
87
92
  #
88
- # :service => %w{800 840 842 844 848}, :mobile => %w{74 76 77 78 79}
89
- swiss_service_regex = /^(800|840|842|844|848)\d+$/
90
- country '41', match(swiss_service_regex) >> split(3,3) |
91
- fixed(2) >> split(3,2,2)
93
+ country '41',
94
+ match(/^(8(00|40|42|44|48))\d+$/) >> split(3,3) |
95
+ fixed(2) >> split(3,2,2)
92
96
 
93
97
 
94
98
  # country '43' # Austria, see special file.
@@ -96,61 +100,71 @@ Phony.define do
96
100
 
97
101
  # Denmark.
98
102
  #
99
- country '45', none >> split(2,2,2,2)
103
+ country '45',
104
+ none >> split(2,2,2,2)
100
105
 
101
106
  # country '46' # Sweden, see special file.
102
107
 
103
108
  # Norway.
104
109
  #
105
- country '47', none >> matched_split(/^[1].*$/ => [3], /^[489].*$/ => [3,2,3], :fallback => [2,2,2,2])
110
+ country '47',
111
+ none >> matched_split(/^[1].*$/ => [3],
112
+ /^[489].*$/ => [3,2,3],
113
+ :fallback => [2,2,2,2])
106
114
 
107
115
  # Poland (Republic of)
108
116
  # Although the NDCs are 2 digits, the representation is 3 digits.
109
117
  # Note: http://wapedia.mobi/en/Telephone_numbers_in_Poland, mobile not yet correct
110
118
  #
111
- country '48', fixed(3) >> split(3,3) # Poland
119
+ country '48',
120
+ fixed(3) >> split(3,3) # Poland
112
121
 
113
122
  # country '49' # Germany, see special file.
114
123
 
115
124
  # Peru.
116
125
  #
117
- country '51', one_of('103', '105') >> split(3,3) | # Service.
118
- one_of('1', '9') >> split(4,4) | # Lima and mobile.
119
- fixed(2) >> split(4,4) # 2-digit NDCs.
126
+ country '51',
127
+ one_of('103', '105') >> split(3,3) | # Service.
128
+ one_of('1', '9') >> split(4,4) | # Lima and mobile.
129
+ fixed(2) >> split(4,4) # 2-digit NDCs.
120
130
 
121
131
  # Mexico.
122
132
  #
123
- country '52', match(/^(0\d{2})\d+$/) >> split(2,2,2,2) |
124
- match(/^(33|55|81)\d+$/) >> split(2,2,2,2) |
125
- match(/^(\d{3})\d+$/) >> split(3,2,2)
133
+ country '52',
134
+ match(/^(0\d{2})\d+$/) >> split(2,2,2,2) |
135
+ match(/^(33|55|81)\d+$/) >> split(2,2,2,2) |
136
+ match(/^(\d{3})\d+$/) >> split(3,2,2)
126
137
 
127
138
  # Cuba.
128
139
  #
129
- country '53', match(/^(5\d{3})\d+$/) >> split(4) | # Mobile
130
- match(/^(7|21|22|23|4[1-8]|3[1-3])/) >> split(7) | # Short NDCs
131
- fixed(3) >> split(7) # 3-digit NDCs
140
+ country '53',
141
+ match(/^(5\d{3})\d+$/) >> split(4) | # Mobile
142
+ match(/^(7|21|22|23|4[1-8]|3[1-3])/) >> split(7) | # Short NDCs
143
+ fixed(3) >> split(7) # 3-digit NDCs
132
144
 
133
145
  # Argentine Republic.
134
146
  #
135
- country '54', one_of('11', '911') >> split(4,4) | # Fixed & Mobile
136
- match(/^(22[0137]|237|26[14]|29[179]|34[1235]|35[138]|38[1578])/) >> split(3,4) | # Fixed
137
- match(/^(922[0137]|9237|926[14]|929[179]|934[1235]|935[138]|938[1578])/) >> split(3,4) | # Mobile
138
- match(/^(9\d{4})/) >> split(2,4) | # Mobile
139
- fixed(4) >> split(2,4) # Fixed
147
+ country '54',
148
+ one_of('11', '911') >> split(4,4) | # Fixed & Mobile
149
+ match(/^(22[0137]|237|26[14]|29[179]|34[1235]|35[138]|38[1578])/) >> split(3,4) | # Fixed
150
+ match(/^(922[0137]|9237|926[14]|929[179]|934[1235]|935[138]|938[1578])/) >> split(3,4) | # Mobile
151
+ match(/^(9\d{4})/) >> split(2,4) | # Mobile
152
+ fixed(4) >> split(2,4) # Fixed
140
153
 
141
154
  # Brazil (Federative Republic of).
155
+ # http://en.wikipedia.org/wiki/Telephone_numbers_in_Brazil
142
156
  #
143
157
  brazilian_service = /^(100|128|190|191|192|193|194|197|198|199)\d+$/
144
- country '55', match(brazilian_service) >> split(3,3) | # Service.
145
- fixed(2) >> split(4,4) # NDCs
146
- # :service? => brazilian_service, :mobile? => ?
147
- # http://en.wikipedia.org/wiki/Telephone_numbers_in_Brazil
158
+ country '55',
159
+ match(brazilian_service) >> split(3,3) | # Service.
160
+ fixed(2) >> split(4,4) # NDCs
148
161
 
149
162
  # Chile.
150
163
  #
151
- country '56', match(/^(13[0-79]|14[79])\d+$/) >> split(3,3) | # Service
152
- one_of('2', '9') >> split(8) | # Santiago, Mobile
153
- fixed(2) >> split(8) # 2-digit NDCs
164
+ country '56',
165
+ match(/^(13[0-79]|14[79])\d+$/) >> split(3,3) | # Service
166
+ one_of('2', '9') >> split(8) | # Santiago, Mobile
167
+ fixed(2) >> split(8) # 2-digit NDCs
154
168
 
155
169
  # TODO Colombia.
156
170
  #
@@ -158,14 +172,17 @@ Phony.define do
158
172
 
159
173
  # Venezuela (Bolivarian Republic of)
160
174
  #
161
- country '58', fixed(3) >> split(7)
175
+ country '58',
176
+ fixed(3) >> split(7)
162
177
 
163
178
  # country '60' # Malaysia, see special file.
164
179
 
165
180
  # Australia.
166
181
  #
167
- country '61', match(/^(4\d\d)\d+$/) >> split(3,3) | # Mobile
168
- fixed(1) >> split(4,4) # Rest
182
+ country '61',
183
+ match(/^(4\d\d)\d+$/) >> split(3,3) | # Mobile
184
+ fixed(1) >> split(4,4) # Rest
185
+
169
186
  country '62', todo # TODO Indonesia (Republic of)
170
187
  country '63', todo # TODO Philippines (Republic of the)
171
188
 
@@ -173,16 +190,19 @@ Phony.define do
173
190
  #
174
191
  # TODO Mobile?
175
192
  #
176
- country '64', fixed(1) >> split(3,4)
193
+ country '64',
194
+ fixed(1) >> split(3,4)
177
195
 
178
196
  # Singapore (Republic of).
179
197
  #
180
- country '65', none >> split(4,4) # TODO Short Codes.
198
+ country '65',
199
+ none >> split(4,4) # TODO Short Codes.
181
200
 
182
201
  # Thailand.
183
202
  #
184
- country '66', one_of('2') >> split(3,4) | # Bangkok
185
- fixed(2) >> split(3,3) # Rest
203
+ country '66',
204
+ one_of('2') >> split(3,4) | # Bangkok
205
+ fixed(2) >> split(3,3) # Rest
186
206
 
187
207
  country '81', todo # TODO Japan
188
208
 
@@ -194,7 +214,8 @@ Phony.define do
194
214
 
195
215
  # Turkey.
196
216
  #
197
- country '90', fixed(3) >> split(3,4) # Wiki says 7, but the examples say 3, 4.
217
+ country '90',
218
+ fixed(3) >> split(3,4) # Wiki says 7, but the examples say 3, 4.
198
219
 
199
220
  country '91', todo # TODO India (Republic of)
200
221
  country '92', todo # TODO Pakistan (Islamic Republic of), http://en.wikipedia.org/wiki/Telephone_numbers_in_Pakistan, NDC 2-5
@@ -264,11 +285,20 @@ Phony.define do
264
285
  country '252', todo # Somali Democratic Republic
265
286
  country '253', todo # Djibouti
266
287
  country '254', fixed(2) >> split(7) # Kenya
267
- country '255', match(/^([89]\d\d)/) >> split(3,3) | # Special/Premium. Tanzania
268
- one_of('112', '118') >> split(3,3) | # Short Codes.
269
- fixed(2) >> split(3,4) # Geographic.
270
- country '256', match(/^(46[45]|4[78]\d)/) >> split(6) | # Geo 1. Uganda
271
- fixed(2) >> split(7) # Geo 2.
288
+
289
+ # Tanzania.
290
+ #
291
+ country '255',
292
+ match(/^([89]\d\d)/) >> split(3,3) | # Special/Premium.
293
+ one_of('112', '118') >> split(3,3) | # Short Codes.
294
+ fixed(2) >> split(3,4) # Geographic.
295
+
296
+ # Uganda.
297
+ #
298
+ country '256',
299
+ match(/^(46[45]|4[78]\d)/) >> split(6) | # Geo 1.
300
+ fixed(2) >> split(7) # Geo 2.
301
+
272
302
  country '257', todo # Burundi
273
303
  country '258', todo # Mozambique
274
304
  country '259', todo # -
@@ -310,10 +340,11 @@ Phony.define do
310
340
 
311
341
  # Portugal.
312
342
  #
313
- country '351', one_of('700', '800') >> split(3,3) | # Service.
314
- match(/^(9\d)\d+$/) >> split(3,4) | # Mobile.
315
- one_of('21', '22') >> split(3,4) | # Lisboa & Porto
316
- fixed(3) >> split(3,4) # 3-digit NDCs
343
+ country '351',
344
+ one_of('700', '800') >> split(3,3) | # Service.
345
+ match(/^(9\d)\d+$/) >> split(3,4) | # Mobile.
346
+ one_of('21', '22') >> split(3,4) | # Lisboa & Porto
347
+ fixed(3) >> split(3,4) # 3-digit NDCs
317
348
 
318
349
  country '352', todo # Luxembourg
319
350
 
@@ -326,19 +357,25 @@ Phony.define do
326
357
 
327
358
  # Finland.
328
359
  #
329
- country '358', match(/^([6-8]00)\d+$/) >> split(3,3) | # Service
330
- match(/^(4\d|50)\d+$/) >> split(3,2,2) | # Mobile
331
- one_of('2','3','5','6','8','9') >> split(3,3) | # Short NDCs
332
- fixed(2) >> split(3,3) # 2-digit NDCs
333
- country '359', fixed(2) >> split(3,2,2) # Bulgaria
360
+ country '358',
361
+ match(/^([6-8]00)\d+$/) >> split(3,3) | # Service
362
+ match(/^(4\d|50)\d+$/) >> split(3,2,2) | # Mobile
363
+ one_of('2','3','5','6','8','9') >> split(3,3) | # Short NDCs
364
+ fixed(2) >> split(3,3) # 2-digit NDCs
365
+
366
+ # Bulgaria.
367
+ #
368
+ country '359',
369
+ fixed(2) >> split(3,2,2) # Bulgaria
334
370
 
335
371
  # Lithuania.
336
372
  #
337
- country '370', one_of('700', '800') >> split(2,3) | # Service
338
- match(/^(6\d\d)\d+$/) >> split(2,3) | # Mobile
339
- one_of('5') >> split(3,2,2) | # Vilnius
340
- one_of('37','41') >> split(2,2,2) | # Kaunas, Šiauliai
341
- fixed(3) >> split(1,2,2) # 3-digit NDCs.
373
+ country '370',
374
+ one_of('700', '800') >> split(2,3) | # Service
375
+ match(/^(6\d\d)\d+$/) >> split(2,3) | # Mobile
376
+ one_of('5') >> split(3,2,2) | # Vilnius
377
+ one_of('37','41') >> split(2,2,2) | # Kaunas, Šiauliai
378
+ fixed(3) >> split(1,2,2) # 3-digit NDCs.
342
379
 
343
380
  country '371', todo # Latvia
344
381
  country '372', todo # Estonia
@@ -6,7 +6,7 @@ module Phony
6
6
  #
7
7
  class CountryCodes
8
8
 
9
- attr_reader :mapping
9
+ attr_reader :splitter_mapping
10
10
  attr_accessor :international_absolute_format, :international_relative_format, :national_format
11
11
 
12
12
  def initialize
@@ -22,11 +22,14 @@ module Phony
22
22
  @instance ||= new
23
23
  end
24
24
 
25
- @@normalizing_pattern = /^0+|\D/
26
- def normalize number
25
+ @@basic_normalizing_pattern = /^0+|\D/
26
+ def clean number
27
27
  # Remove non-digit chars.
28
28
  #
29
- number.gsub! @@normalizing_pattern, EMPTY_STRING
29
+ number.gsub! @@basic_normalizing_pattern, EMPTY_STRING
30
+ end
31
+ def normalize number
32
+ clean number
30
33
  national_handler, cc, rest = split_cc number
31
34
  @normalize_format % [cc, national_handler.normalize(rest)]
32
35
  end
@@ -102,7 +105,7 @@ module Phony
102
105
  presumed_cc = ''
103
106
  1.upto(3) do |i|
104
107
  presumed_cc << rest.slice!(0..0)
105
- national_code_handler = mapping[i][presumed_cc]
108
+ national_code_handler = splitter_mapping[i][presumed_cc]
106
109
  return [national_code_handler, presumed_cc, rest] if national_code_handler
107
110
  end
108
111
  # This line is never reached as CCs are in prefix code.
@@ -121,9 +124,9 @@ module Phony
121
124
  country_code = country_code.to_s
122
125
  optimized_country_code_access = country_code.size
123
126
 
124
- @mapping ||= {}
125
- @mapping[optimized_country_code_access] ||= {}
126
- @mapping[optimized_country_code_access][country_code] = country
127
+ @splitter_mapping ||= {}
128
+ @splitter_mapping[optimized_country_code_access] ||= {}
129
+ @splitter_mapping[optimized_country_code_access][country_code] = country
127
130
  end
128
131
 
129
132
  end
data/lib/phony/dsl.rb CHANGED
@@ -46,8 +46,9 @@ module Phony
46
46
 
47
47
  #
48
48
  #
49
- def country country_code, country
49
+ def country country_code, country, validator = nil
50
50
  Phony::CountryCodes.instance.add country_code, country
51
+ Phony::Validators.instance.add country_code, validator if validator
51
52
  end
52
53
 
53
54
  # National matcher & splitters.
@@ -89,6 +90,12 @@ module Phony
89
90
  def matched_split options = {}
90
91
  Phony::LocalSplitters::Regex.instance_for options
91
92
  end
93
+
94
+ # Validators
95
+ #
96
+ def invalid_ndcs ndc
97
+ Validator.new.ndc_check ndc
98
+ end
92
99
 
93
100
  end
94
101
 
@@ -0,0 +1,26 @@
1
+ module Phony
2
+
3
+ class Validator
4
+
5
+ attr_reader :ndc_checks
6
+
7
+ def initialize
8
+ @ndc_checks = []
9
+ end
10
+
11
+ def plausible? ndc, rest
12
+ ndc_checks && ndc_checks.each do |ndc_check|
13
+ return false if ndc_check === ndc
14
+ end
15
+
16
+ true
17
+ end
18
+
19
+ def ndc_check ndc
20
+ @ndc_checks << ndc
21
+ self
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,77 @@
1
+ # # Number: A possible phone number, E164 or not.
2
+ # # Hints: Information that helps or constricts the plausibility check.
3
+ # #
4
+ # plausible? number, hints = {}
5
+ #
6
+
7
+ # plausible? number # Uses the definitions from the country definition to plausibility check.
8
+ # plausible? number, cc: 1 # => Checks cc.
9
+ # plausible? number, pattern: /[^5]/ # Uses def, checks against split.
10
+ # plausible? number, country: 1, pattern: [3, 4, 3] # Uses given country – adds cc.
11
+ #
12
+
13
+ # Basic plausibility is:
14
+ # * Max digits are 15.
15
+ # * Min digits are 2 (?)
16
+ #
17
+
18
+ module Phony
19
+
20
+ class Validators
21
+
22
+ def initialize
23
+ @validators = {}
24
+ end
25
+
26
+ def self.instance
27
+ @instance ||= new
28
+ end
29
+
30
+ # Add a specific country validator.
31
+ #
32
+ def add cc, validator
33
+ @validators[cc] = validator
34
+ end
35
+
36
+ # Is the given number plausible?
37
+ #
38
+ def plausible? number, hints = {}
39
+ normalized = CountryCodes.instance.clean number
40
+
41
+ # False if it fails the basic check.
42
+ #
43
+ return false unless (2..15) === normalized.size
44
+
45
+ # Hint based checking.
46
+ #
47
+ cc, ndc, *rest = Phony.split normalized
48
+
49
+ # CC.
50
+ #
51
+ cc_needed = hints[:cc]
52
+ return false if cc_needed && cc_needed != cc
53
+
54
+ # NDC.
55
+ #
56
+ ndc_needed = hints[:ndc]
57
+ return false if ndc_needed && ndc_needed != ndc
58
+
59
+ # Country specific checks.
60
+ #
61
+ validator = validator_for cc
62
+ validator.plausible? ndc, rest
63
+ rescue StandardError
64
+ return false
65
+ end
66
+
67
+ def validator_for cc
68
+ @validators[cc] || default_validator
69
+ end
70
+
71
+ def default_validator
72
+ @default_validator ||= Validator.new
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'spec_helper'
4
+
5
+ describe 'validations' do
6
+
7
+ describe 'plausible?' do
8
+
9
+ it "is correct" do
10
+ Phony.plausible?('0000000').should be_false
11
+ end
12
+ it "is correct" do
13
+ Phony.plausible?('hello').should be_false
14
+ end
15
+
16
+ it "is correct" do
17
+ Phony.plausible?('+41 44 111 22 33').should be_true
18
+ end
19
+ it "is correct for explicit checks" do
20
+ Phony.plausible?('+41 44 111 22 33', cc: '41').should be_true
21
+ end
22
+ it "is correct for explicit checks" do
23
+ Phony.plausible?('+41 44 111 22 33', ndc: '44').should be_true
24
+ end
25
+ it "is correct for explicit checks" do
26
+ Phony.plausible?('+41 44 111 22 33', cc: '1').should be_false
27
+ end
28
+ it "is correct for explicit checks" do
29
+ Phony.plausible?('+41 44 111 22 33', ndc: '43').should be_false
30
+ end
31
+
32
+ context 'countries' do
33
+
34
+ it "is correct for US numbers" do
35
+ Phony.plausible?('1-800-692-7753').should be_true
36
+ Phony.plausible?('1-911').should be_false
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phony
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.5
4
+ version: 1.6.6
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-15 00:00:00.000000000 Z
12
+ date: 2012-03-18 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: ! 'Fast international phone number (E164 standard) normalizing, splitting
15
15
  and formatting. Lots of formatting options: International (+.., 00..), national
@@ -43,6 +43,8 @@ files:
43
43
  - lib/phony/national_splitters/none.rb
44
44
  - lib/phony/national_splitters/regex.rb
45
45
  - lib/phony/national_splitters/variable.rb
46
+ - lib/phony/validator.rb
47
+ - lib/phony/validators.rb
46
48
  - lib/phony/vanity.rb
47
49
  - lib/phony.rb
48
50
  - README.textile
@@ -57,6 +59,7 @@ files:
57
59
  - spec/lib/phony/national_splitters/none_spec.rb
58
60
  - spec/lib/phony/national_splitters/regex_spec.rb
59
61
  - spec/lib/phony/national_splitters/variable_spec.rb
62
+ - spec/lib/phony/validations_spec.rb
60
63
  - spec/lib/phony/vanity_spec.rb
61
64
  - spec/lib/phony_spec.rb
62
65
  homepage: http://github.com/floere/phony
@@ -96,5 +99,6 @@ test_files:
96
99
  - spec/lib/phony/national_splitters/none_spec.rb
97
100
  - spec/lib/phony/national_splitters/regex_spec.rb
98
101
  - spec/lib/phony/national_splitters/variable_spec.rb
102
+ - spec/lib/phony/validations_spec.rb
99
103
  - spec/lib/phony/vanity_spec.rb
100
104
  - spec/lib/phony_spec.rb