sophone
Version:
πΈπ΄ Professional Somali phone number validation, formatting, operator detection & mobile wallet identification. Supports EVC, Sahal, ZAAD, eDahab, and Jeeb with CLI tools, TypeScript support, and beautiful error handling.
677 lines (517 loc) β’ 19.4 kB
Markdown
# πΈπ΄ sophone
**Professional Somali phone number validation, formatting, operator detection & mobile wallet identification**
A comprehensive JavaScript/TypeScript library for working with Somali phone numbers. Validate, format, identify operators, and detect mobile money wallets (EVC, Sahal, ZAAD, eDahab, Jeeb) for Somalia (+252) phone numbers with beautiful error handling and TypeScript support.
[](https://www.npmjs.com/package/sophone)
[](https://opensource.org/licenses/MIT)
[](https://github.com/omartood/sophone/actions)
## β¨ Features
- π **Validate** Somali phone numbers in any format
- π¨ **Format** numbers to E.164, local, or international format
- π± **Identify operators** (Hormuud, Somtel, Telesom, etc.)
- π° **Detect mobile wallets** (EVC, Sahal, ZAAD, eDahab, Jeeb)
- π‘οΈ **TypeScript support** with complete type definitions
- π **CLI tool** for command-line usage
- π¦ **Batch processing** for multiple numbers
- π― **Beautiful error handling** with detailed messages
- β‘ **Zero dependencies** and lightweight
## π¦ Installation
### For Library Use
```bash
npm install sophone
```
### For CLI Use (Global)
```bash
npm install -g sophone
```
## π Quick Start
### Basic Usage
```javascript
import {
isValidSomaliMobile,
normalizeE164,
formatLocal,
getOperator,
getWallet,
getWalletInfo,
} from "sophone";
// Validate any Somali phone number format
isValidSomaliMobile("+252 61 123 4567"); // β
true
isValidSomaliMobile("0611234567"); // β
true
isValidSomaliMobile("611234567"); // β
true
isValidSomaliMobile("252611234567"); // β
true
isValidSomaliMobile("invalid"); // β false
// Convert to international E.164 format
normalizeE164("0611234567"); // "+252611234567"
// Format for local display
formatLocal("+252611234567"); // "0611 234 567"
// Identify the mobile operator
getOperator("0611234567"); // "Hormuud"
// Identify the mobile wallet
getWallet("0611234567"); // "EVC"
getWallet("0621234567"); // "Sahal"
getWallet("0631234567"); // "ZAAD"
// Get detailed wallet information
const walletInfo = getWalletInfo("0611234567");
console.log(walletInfo.name); // "EVC Plus"
console.log(walletInfo.ussd); // "*770#"
```
## π Comprehensive Usage Guide
### 1. Phone Number Validation
```javascript
import { isValidSomaliMobile, validate } from "sophone";
// Simple validation (boolean result)
const phoneNumbers = [
"+252 61 123 4567", // Hormuud
"0621234567", // Somtel
"631234567", // Telesom
"invalid-number", // Invalid
"123", // Too short
];
phoneNumbers.forEach((number) => {
const isValid = isValidSomaliMobile(number);
console.log(`${number}: ${isValid ? "β
Valid" : "β Invalid"}`);
});
// Detailed validation with error information
const result = validate("0611234567");
if (result.ok) {
console.log("β
Valid number!");
console.log(`π± Original: ${result.value.input}`);
console.log(`π International: ${result.value.e164}`);
console.log(`π Local: ${result.value.local}`);
console.log(`π‘ Operator: ${result.value.operator}`);
console.log(`π° Wallet: ${result.value.wallet || 'None'}`);
} else {
console.log("β Invalid number!");
console.log(`π¨ Error: ${result.error.message}`);
console.log(`π Code: ${result.error.code}`);
}
```
### 2. Number Formatting
```javascript
import {
normalizeE164,
formatLocal,
formatInternational,
// Safe versions that return null instead of throwing
normalizeE164Safe,
formatLocalSafe,
formatInternationalSafe,
} from "sophone";
const phoneNumber = "0611234567";
// Different formatting options
console.log("π± Original:", phoneNumber);
console.log("π E.164:", normalizeE164(phoneNumber)); // "+252611234567"
console.log("π Local:", formatLocal(phoneNumber)); // "0611 234 567"
console.log("π International:", formatInternational(phoneNumber)); // "+252 61 123 4567"
// Safe formatting (won't throw errors)
const invalidNumber = "invalid";
console.log("Safe E.164:", normalizeE164Safe(invalidNumber)); // null
console.log("Safe Local:", formatLocalSafe(invalidNumber)); // null
```
### 3. Operator Detection
```javascript
import { getOperator, getOperatorInfo, getAllOperators } from "sophone";
// Get operator name
console.log(getOperator("0611234567")); // "Hormuud"
console.log(getOperator("0621234567")); // "Somtel"
console.log(getOperator("0631234567")); // "Telesom"
// Get detailed operator information
const operatorInfo = getOperatorInfo("0611234567");
console.log("π‘ Operator:", operatorInfo.name); // "Hormuud Telecom Somalia"
console.log("π Prefixes:", operatorInfo.prefixes); // ["61", "77"]
console.log("π Website:", operatorInfo.website); // "https://hormuud.com"
console.log("π± Type:", operatorInfo.type); // "GSM"
console.log("π° Primary Wallet:", operatorInfo.wallet); // "EVC"
// List all available operators
const allOperators = getAllOperators();
allOperators.forEach((op) => {
console.log(`π‘ ${op.name} (${op.prefixes.join(", ")}) - Wallet: ${op.wallet || 'None'}`);
});
```
### 4. Mobile Wallet Detection
```javascript
import {
getWallet,
getWalletInfo,
getAllWallets,
getWalletByName,
getWalletByOperator,
getSupportedWallets
} from "sophone";
// Get wallet name
console.log(getWallet("0611234567")); // "EVC"
console.log(getWallet("0621234567")); // "Sahal"
console.log(getWallet("0631234567")); // "ZAAD"
// Get detailed wallet information
const evcInfo = getWalletInfo("0611234567");
console.log("π° Wallet:", evcInfo.name); // "EVC Plus"
console.log("π Full Name:", evcInfo.fullName); // "Electronic Virtual Cash Plus"
console.log("π‘ Operator:", evcInfo.operator); // "Hormuud"
console.log("π USSD:", evcInfo.ussd); // "*770#"
console.log("π Website:", evcInfo.website); // "https://evcplus.so"
console.log("β‘ Features:", evcInfo.features); // ["Send Money", "Receive Money", ...]
// Get wallet by name
const zaadWallet = getWalletByName("ZAAD");
console.log("π° ZAAD USSD:", zaadWallet.ussd); // "*712#"
// Get wallet by operator
const hormuudWallet = getWalletByOperator("Hormuud");
console.log("π° Hormuud Wallet:", hormuudWallet.name); // "EVC Plus"
// List all supported wallets
const supportedWallets = getSupportedWallets();
console.log("π° Supported Wallets:", supportedWallets); // ["EVC", "Sahal", "ZAAD", "eDahab", "Jeeb"]
// Get all wallet details
const allWallets = getAllWallets();
allWallets.forEach((wallet) => {
console.log(`π° ${wallet.name} (${wallet.operator}) - USSD: ${wallet.ussd || 'N/A'}`);
});
```
### 5. Error Handling
```javascript
import { normalizeE164, SomaliPhoneError, ERROR_CODES } from "sophone";
// Throwing functions with proper error handling
function processPhoneNumber(input) {
try {
const e164 = normalizeE164(input);
console.log(`β
Processed: ${e164}`);
return e164;
} catch (error) {
if (error instanceof SomaliPhoneError) {
switch (error.code) {
case ERROR_CODES.INVALID_LENGTH:
console.log(`β Wrong length: ${error.message}`);
break;
case ERROR_CODES.INVALID_PREFIX:
console.log(`β Invalid prefix: ${error.message}`);
break;
case ERROR_CODES.INVALID_INPUT:
console.log(`β Invalid input: ${error.message}`);
break;
default:
console.log(`β Unknown error: ${error.message}`);
}
// Access additional error details
console.log("π Error details:", error.details);
}
return null;
}
}
// Test with different inputs
processPhoneNumber("0611234567"); // β
Valid
processPhoneNumber("123"); // β Too short
processPhoneNumber("0111234567"); // β Invalid prefix
processPhoneNumber("invalid"); // β Invalid input
```
### 6. Batch Processing
```javascript
import { validateBatch, normalizeBatch } from "sophone";
const phoneNumbers = [
"0611234567", // Valid Hormuud
"0621234567", // Valid Somtel
"invalid", // Invalid
"123", // Too short
"0771234567", // Valid Hormuud
];
// Validate multiple numbers at once
const validationResults = validateBatch(phoneNumbers);
validationResults.forEach((result) => {
if (result.ok) {
console.log(
`β
${result.input} β ${result.value.e164} (${result.value.operator})`
);
} else {
console.log(`β ${result.input} β ${result.error.message}`);
}
});
// Normalize multiple numbers (safe operation)
const normalizedResults = normalizeBatch(phoneNumbers);
normalizedResults.forEach((result) => {
console.log(`${result.input} β ${result.result || "Invalid"}`);
});
```
### 7. Real-World Examples
#### User Registration Form Validation
```javascript
import { isValidSomaliMobile, validate, formatLocal } from "sophone";
function validateUserPhone(phoneInput) {
// Quick validation
if (!isValidSomaliMobile(phoneInput)) {
return {
valid: false,
message: "Please enter a valid Somali phone number",
};
}
// Get detailed information
const result = validate(phoneInput);
if (result.ok) {
return {
valid: true,
formatted: result.value.local,
operator: result.value.operator,
wallet: result.value.wallet,
e164: result.value.e164,
};
}
return { valid: false, message: result.error.message };
}
// Usage in form validation
const userInput = "0611234567";
const validation = validateUserPhone(userInput);
if (validation.valid) {
console.log(`β
Phone: ${validation.formatted}`);
console.log(`π‘ Operator: ${validation.operator}`);
console.log(`π° Wallet: ${validation.wallet || 'No primary wallet'}`);
// Store validation.e164 in database
} else {
console.log(`β Error: ${validation.message}`);
}
```
#### Contact List Formatter
```javascript
import { formatLocal, getOperator, getWallet, isValidSomaliMobile } from "sophone";
const contacts = [
{ name: "Ahmed", phone: "+252611234567" },
{ name: "Fatima", phone: "0621234567" },
{ name: "Omar", phone: "631234567" },
{ name: "Amina", phone: "invalid-phone" },
];
const formattedContacts = contacts.map((contact) => {
if (isValidSomaliMobile(contact.phone)) {
return {
...contact,
formattedPhone: formatLocal(contact.phone),
operator: getOperator(contact.phone),
wallet: getWallet(contact.phone),
valid: true,
};
} else {
return {
...contact,
formattedPhone: contact.phone,
operator: null,
wallet: null,
valid: false,
};
}
});
console.log("π Contact List:");
formattedContacts.forEach((contact) => {
const status = contact.valid ? "β
" : "β";
const operator = contact.operator ? `(${contact.operator})` : "";
const wallet = contact.wallet ? ` | ${contact.wallet}` : "";
console.log(
`${status} ${contact.name}: ${contact.formattedPhone} ${operator}${wallet}`
);
});
```
### Throwing vs Safe Functions
The library provides both throwing and safe (non-throwing) versions of functions:
```javascript
// Throwing functions (throw SomaliPhoneError on invalid input)
try {
normalizeE164("0611234567"); // "+252611234567"
formatLocal("0611234567"); // "0611 234 567"
getOperator("0611234567"); // "Hormuud"
} catch (error) {
if (error instanceof SomaliPhoneError) {
console.log(error.code); // ERROR_CODES.INVALID_PREFIX
console.log(error.message); // Descriptive message
}
}
// Safe functions (return null on invalid input, never throw)
normalizeE164Safe("0611234567"); // "+252611234567"
normalizeE164Safe("invalid"); // null
formatLocalSafe("0611234567"); // "0611 234 567"
formatLocalSafe("invalid"); // null
getOperatorSafe("0611234567"); // "Hormuud"
getOperatorSafe("invalid"); // null
```
### Error Handling
The library uses a custom `SomaliPhoneError` class with structured error codes:
```javascript
try {
normalizeE164("invalid");
} catch (error) {
console.log(error instanceof SomaliPhoneError); // true
console.log(error.code); // "INVALID_INPUT"
console.log(error.message); // "invalid" contains no valid digits
console.log(error.details); // Additional context
}
// Available error codes
console.log(ERROR_CODES.INVALID_INPUT); // "INVALID_INPUT"
console.log(ERROR_CODES.INVALID_LENGTH); // "INVALID_LENGTH"
console.log(ERROR_CODES.INVALID_PREFIX); // "INVALID_PREFIX"
console.log(ERROR_CODES.UNKNOWN); // "UNKNOWN"
```
## π₯οΈ CLI Usage
The CLI tool provides a convenient way to work with Somali phone numbers from the command line.
### Installation
```bash
npm install -g sophone
```
### Commands
```bash
# Validate a phone number
sophone validate "+252 61 123 4567"
# Output: β valid
sophone validate "invalid-number"
# Output: β invalid
# "invalid-number" contains no valid digits
# Format to different formats
sophone format "0611234567" # 0611 234 567
sophone e164 "0611234567" # +252611234567
sophone international "0611234567" # +252 61 123 4567
# Get operator and wallet information
sophone operator "0611234567" # Hormuud
sophone wallet "0611234567" # EVC
sophone info "0611234567"
# Output: Operator: Hormuud Telecom Somalia
# Prefixes: 61, 77
# Type: GSM
# Website: https://hormuud.com
# Primary Wallet: EVC
sophone walletinfo "0611234567"
# Output: Wallet: EVC Plus
# Full Name: Electronic Virtual Cash Plus
# Operator: Hormuud
# Description: Hormuud's flagship mobile money service
# Features: Send Money, Receive Money, Pay Bills, Buy Airtime, Merchant Payments
# USSD Code: *770#
# Website: https://evcplus.so
# List all operators and wallets
sophone operators
# Output: Available Operators:
# Hormuud Telecom Somalia (61, 77)
# Website: https://hormuud.com
# Primary Wallet: EVC
# Somtel Network (62, 65, 66)
# Website: https://somtel.com
# Primary Wallet: Sahal
# ...
sophone wallets
# Output: Available Mobile Wallets:
# EVC Plus (Hormuud)
# USSD: *770#
# Website: https://evcplus.so
# Features: Send Money, Receive Money, Pay Bills, Buy Airtime, Merchant Payments
# Sahal (Somtel)
# USSD: *828#
# Website: https://somtel.com/sahal
# Features: Money Transfer, Bill Payment, Airtime Purchase, Merchant Services
# ...
# Process multiple numbers from a file
echo -e "0611234567\n0621234567\ninvalid\n123" > numbers.txt
sophone batch numbers.txt
# Output: Processing 4 numbers from numbers.txt:
# β 0611234567 β +252611234567 (Hormuud | EVC)
# β 0621234567 β +252621234567 (Somtel | Sahal)
# β invalid β "invalid" contains no valid digits
# β 123 β "123" is too short (3 digits)
# Summary: 2 valid, 2 invalid
# Get help
sophone help
```
### CLI Examples in Scripts
```bash
#!/bin/bash
# Validate and format phone numbers in a script
phone="0611234567"
if sophone validate "$phone" > /dev/null 2>&1; then
echo "Phone number is valid!"
formatted=$(sophone format "$phone")
operator=$(sophone operator "$phone")
echo "Formatted: $formatted"
echo "Operator: $operator"
else
echo "Invalid phone number: $phone"
fi
```
## Supported Operators & Mobile Wallets
| Operator | Prefixes | Primary Wallet | USSD Code |
|----------|----------|----------------|-----------|
| **Hormuud Telecom** | 61, 77 | EVC Plus | *770# |
| **Somtel Network** | 62, 65, 66 | Sahal | *828# |
| **Telesom** | 63 | ZAAD | *712# |
| **SomLink** | 64 | - | - |
| **SomNet** | 68 | - | - |
| **NationLink** | 69 | - | - |
| **Amtel** | 71 | - | - |
### Additional Mobile Wallets
- **eDahab**: Multi-operator service (*444#) - International remittance
- **Jeeb**: Multi-operator digital wallet - E-commerce focused
## API
### Core Functions
#### `isValidSomaliMobile(input: string): boolean`
Validates if the input is a valid Somali mobile number. Never throws errors.
#### `validate(input: string): ValidationResult`
Comprehensive validation that returns `{ ok: true, value: {...} }` for valid numbers or `{ ok: false, error: {...} }` for invalid ones.
### Throwing Functions
These functions throw `SomaliPhoneError` for invalid input:
#### `normalizeE164(input: string): string`
Normalizes a Somali mobile number to E.164 format (+252XXXXXXXXX).
#### `formatLocal(input: string): string`
Formats a Somali mobile number to local format (0XXX XXX XXX).
#### `getOperator(input: string): string | null`
Gets the mobile operator for a Somali mobile number.
#### `getWallet(input: string): string | null`
Gets the primary mobile wallet for a Somali mobile number based on the operator.
#### `getWalletInfo(input: string): WalletInfo | null`
Gets detailed wallet information including name, USSD code, features, and website.
### Safe Functions (Non-throwing)
These functions return `null` instead of throwing errors:
#### `normalizeE164Safe(input: string): string | null`
Safe version of `normalizeE164`.
#### `formatLocalSafe(input: string): string | null`
Safe version of `formatLocal`.
#### `getOperatorSafe(input: string): string | null`
Safe version of `getOperator`.
#### `getWalletSafe(input: string): string | null`
Safe version of `getWallet`.
#### `getWalletInfoSafe(input: string): WalletInfo | null`
Safe version of `getWalletInfo`.
### Utility Functions
#### `getAllWallets(): Array<WalletInfo>`
Returns an array of all supported mobile wallets with detailed information.
#### `getWalletByName(walletName: string): WalletInfo | null`
Gets wallet information by wallet name (e.g., "EVC", "Sahal").
#### `getWalletByOperator(operatorName: string): WalletInfo | null`
Gets wallet information by operator name (e.g., "Hormuud", "Somtel").
#### `getSupportedWallets(): Array<string>`
Returns an array of supported wallet names.
### Error Handling
#### `SomaliPhoneError`
Custom error class with `code`, `message`, and `details` properties.
#### `ERROR_CODES`
Constants for error codes:
- `INVALID_INPUT`: Invalid or missing input
- `INVALID_LENGTH`: Wrong number of digits
- `INVALID_PREFIX`: Unrecognized mobile prefix
- `UNKNOWN`: General unknown error
## Links
- **GitHub Repository**: https://github.com/omartood/sophone
- **npm Package**: https://www.npmjs.com/package/sophone
- **Issues**: https://github.com/omartood/sophone/issues
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## Development
```bash
# Clone the repository
git clone https://github.com/omartood/sophone.git
cd sophone
# Install dependencies
npm install
# Run tests
npm test
# Test CLI locally
node src/cli.js validate "0611234567"
```
## Changelog
See [CHANGELOG.md](https://github.com/omartood/sophone/blob/main/CHANGELOG.md) for details.
## License
MIT - see [LICENSE](https://github.com/omartood/sophone/blob/main/LICENSE) file for details.
---
Created by Omar ToodπΈπ΄