Mask a phone number while preserving the visible structure — country code, separators, length. Apps that validate format on the masked data (regex, length checks, libphonenumber) keep working; analysts can still see "looks like a UK number".
Country code: keep the first digit-group after +. All separators (+, space, dash, parens) stay in their positions. Remaining digits are replaced from a deterministic hash of salt:phone.
Tests this task must pass
Determinism — same input gives the same output.
Format preserved — +7 702 365 6813 output starts with +7 , has 3 spaces, same total length.
Different inputs differ — distinct numbers produce distinct masks.
Null in → null out.
Multi-digit country codes — UK +44 20 7946 0958 output starts with +44 and keeps 3 spaces.
import java.security.MessageDigest
//sampleStart
fun maskPhone(phone: String?, salt: String = "s3cret"): String? {
// Preserve format: spaces, dashes, plus sign stay in same positions
// Replace digits (except country code) with deterministic digits from hash
// Country code: first 1-3 digits after "+" are preserved
// Same input + salt → always same output
TODO()
}
//sampleEnd
fun main() {
// Test 1: Determinism
val a = maskPhone("+7 702 365 6813")
val b = maskPhone("+7 702 365 6813")
check(a == b) { "FAIL: must be deterministic. Got $a and $b" }
println("✅ Test 1: Deterministic — $a")
// Test 2: Format preserved
check(a!!.startsWith("+7 ")) { "FAIL: country code preserved" }
check(a.count { it == ' ' } == 3) { "FAIL: spaces preserved. Got: $a" }
check(a.length == "+7 702 365 6813".length) { "FAIL: length preserved" }
println("✅ Test 2: Format preserved — $a")
// Test 3: Different input → different output
val c = maskPhone("+7 999 111 2222")
check(a != c) { "FAIL: different inputs must differ" }
println("✅ Test 3: Different outputs")
// Test 4: Null handling
check(maskPhone(null) == null) { "FAIL: null → null" }
println("✅ Test 4: Null handling")
// Test 5: Different format preserved
val d = maskPhone("+44 20 7946 0958")
check(d!!.startsWith("+44 ")) { "FAIL: +44 preserved" }
check(d.count { it == ' ' } == 3) { "FAIL: spaces preserved for UK number" }
println("✅ Test 5: UK format — $d")
println("\n🎉 ALL TESTS PASSED!")
}
Hint
Build a digit stream from the SHA-256 hash bytes (e.g., split each byte into two decimal digits). Walk the input character by character: keep +, separators, and country-code digits; replace all other digits with the next digit from the stream.
Solution
fun maskPhone(phone: String?, salt: String = "s3cret"): String? {
if (phone == null) return null
val hash = MessageDigest.getInstance("SHA-256")
.digest("$salt:$phone".toByteArray())
val stream = hash.flatMap {
val v = it.toInt() and 0xFF
listOf((v / 10) % 10, v % 10)
}
var i = 0
val out = StringBuilder()
var inCountry = false
var countryDone = false
for (c in phone) {
when {
c == '+' -> { out.append(c); inCountry = true }
c.isDigit() && inCountry && !countryDone -> out.append(c)
c.isDigit() -> { out.append(stream[i % stream.size]); i++ }
else -> {
if (inCountry) { inCountry = false; countryDone = true }
out.append(c)
}
}
}
return out.toString()
}