2

I am experiencing a problem that I am not sure how to solve and I hope someone here can help me. Currently I have a string variable and later I replace the letters in the string with underscores like the following:

var str = "Hello playground"

let replace = str.replacingOccurrences(of: "\\S", with: "_", options: .regularExpression)

print(str)

Know I would like to randomly generate 25 % of the characters in str (In this case 16 * 0,25 = 4) so it later prints something like these examples:

str = "H__l_ ___yg_____"

str = "_____ play______"

str = "__ll_ ____g____d"

Does anyone have any ideas of how to do this?

2
  • 2
    You could use a NSRegularExpression, get all the matches, and in it, pick 3/4 of them and replace them with "_". Commented Jan 15, 2019 at 15:29
  • Does it have to be exactly 25%? Commented Jan 15, 2019 at 15:46

9 Answers 9

5

A possible solution:

var str = "Hello playground"
print("Before: \(str)")
do {
    let regex = try NSRegularExpression(pattern: "\\S", options: [])
    let matches = regex.matches(in: str, options: [], range: NSRange(location: 0, length: str.utf16.count))

    //Retrieve 1/4 elements of the string
    let randomElementsToReplace = matches.shuffled().dropLast(matches.count * 1/4)

    matches.forEach({ (aMatch) in
        if randomElementsToReplace.first(where: { $0.range == aMatch.range } ) != nil {
            str.replaceSubrange(Range(aMatch.range, in: str)!, with: "_")
        } else {
            //Do nothing because that's the one we are keeping as such
        }
    })
    print("After: \(str)")
} catch {
    print("Error while creating regex: \(error)")
}

The idea behind it: Use the same Regular Expression pattern as the one you used.
Pick up n elements in it (in your case 1/4)
Replace every character that isn't in that short list.

Now that you got the idea, it's even faster replacing the for loop with

for aMatch in randomElementsToReplace {
    str.replaceSubrange(Range(aMatch.range, in: str)!, with: "_")
}

Thanks to @Martin R's comment for pointing it out.

Output (done 10 times):

$>Before: Hello playground
$>After: ____o ___y____n_
$>Before: Hello playground
$>After: _el__ _______u__
$>Before: Hello playground
$>After: _e___ ____g___n_
$>Before: Hello playground
$>After: H___o __a_______
$>Before: Hello playground
$>After: H___o _______u__
$>Before: Hello playground
$>After: __l__ _____ro___
$>Before: Hello playground
$>After: H____ p________d
$>Before: Hello playground
$>After: H_l__ _l________
$>Before: Hello playground
$>After: _____ p____r__n_
$>Before: Hello playground
$>After: H___o _____r____
$>Before: Hello playground
$>After: __l__ ___y____n_

You'll see that there is a little difference from your expected result, it's because matches.count == 15, so 1/4 of them should be what? It's up to you there to do the correct calculation according to your needs (round up?, etc.) since you didn't specified it.

Note that if you don't want to round up, you could also do the reverse, use the randomed for the one to not replace, and then the round might play in your favor.

Sign up to request clarification or add additional context in comments.

4 Comments

Simpler (?): for aMatch in randomElementsToReplace { str.replaceSubrange(Range(aMatch.range, in: str)!, with: "_") }
Indeed, I was focused on explaining the idea behind it that I coded as such and not as simplified.
Is for in really faster than .forEach?
It's just that I did a first(where:) of matches at each iteration, but since we have the NSTextChekingResult we want already, we just need to iterate through them.
3

Similarly as in Replace specific characters in string, you can map each character, and combine the result to a string. But now you have to keep track of the (remaining) numbers of non-space characters, and the (remaining) numbers of characters that should be displayed. For each (non-space) character it is randomly decided whether to display (keep) it or to replace it by an underscore.

let s = "Hello playground"
let factor = 0.25

var n = s.filter({ $0 != " " }).count  // # of non-space characters
var m = lrint(factor * Double(n))      // # of characters to display

let t = String(s.map { c -> Character in
    if c == " " {
        // Preserve space
        return " "
    } else if Int.random(in: 0..<n) < m {
        // Keep
        m -= 1
        n -= 1
        return c
    } else {
        // Replace
        n -= 1
        return "_"
    }
})

print(t) // _e_l_ ______o_n_

Comments

3

This method creates an array of bools that determines which characters will be kept and which will be replaced by using the inbuilt shuffled function.

let string = "Hello playground"
let charsToKeep = string.count / 4
let bools = (Array<Bool>(repeating: true, count: charsToKeep) 
           + Array<Bool>(repeating: false, count: string.count - charsToKeep)).shuffled()

let output = zip(string, bools).map
{
    char, bool in
    return bool ? char : "_"
}

print(String(output))

Edit The above doesn't deal with spaces correctly, but I'll leave it here anyway as a general example.

Here is a version that does deal with the spaces.

let string = "Hello playground and stackoverflow"
let nonSpaces = string.filter{ $0 != " " }.count

let bools = (Array<Bool>(repeating: true, count: nonSpaces / 4) + Array<Bool>(repeating: false, count: nonSpaces - nonSpaces / 4)).shuffled()

var nextBool = bools.makeIterator()
let output = string.map
{
    char in
    return char == " " ? " " : (nextBool.next()! ? char : "_")
}

print(String(output))

// Hel__ __________ a__ __a____e____w
// ___l_ _l__g_____ _n_ __a_____r__o_

Comments

1

Another possible approach is to generate random indexes for the given string and then replace the characters at those indexes:

var str = "Hello, playground"

let indexes: [Int] = Array(0..<str.count)

let randomIndexes = Array(indexes.shuffled()[0..<(str.count / 4)])

for index in randomIndexes {
    let start = str.index(str.startIndex, offsetBy: index)
    let end = str.index(str.startIndex, offsetBy: index+1)
    str.replaceSubrange(start..<end, with: "_")
}

print(str)

If you put this in a extension on String, it would look like:

extension String {

    func randomUnderscores(factor: Double) -> String {
        let indexes: [Int] = Array(0..<count)
        let endIndexes = Int(Double(count) * factor)
        let randomIndexes = Array(indexes.shuffled()[0..<endIndexes])

        var randomized = self

        for index in randomIndexes {
            let start = randomized.index(startIndex, offsetBy: index)
            let end = randomized.index(startIndex, offsetBy: index+1)
            randomized.replaceSubrange(start..<end, with: "_")
        }

        return randomized
    }
}

print(str.randomUnderscores(factor: 0.25))

1 Comment

Note that your code might allow the replacement of comma, space, not only letters (slight differences from the author needs).
1

I just came up with the following solution:

func generateMyString(string: String) -> String {
    let percentage = 0.25

    let numberOfCharsToReplace = Int(floor(Double(string.count) * percentage))

    let generatedString = stride(from: 0, to: string.count, by: 1).map { index -> String in
        return string[string.index(string.startIndex, offsetBy: index)] == " " ? " " : "_"
    }.joined()

    var newString = generatedString
    for i in generateNumbers(repetitions: numberOfCharsToReplace, maxValue: string.count - 1) {
        var newStringArray = Array(newString)
        newStringArray[i] = Array(string)[i]

        newString = String(newStringArray)
    }

    return newString
}

func generateNumbers(repetitions: Int, maxValue: Int) -> [Int] {
    guard maxValue >= repetitions else {
        fatalError("maxValue must be >= repetitions for the numbers to be unique")
    }

    var numbers = [Int]()

    for _ in 0..<repetitions {
        var n: Int
        repeat {
            n = Int.random(in: 1...maxValue)
        } while numbers.contains(n)
        numbers.append(n)
    }

    return numbers
}

Output:

let str = "Hello playground"
print(generateMyString(string: str)) // ___lo _l_______d

4 Comments

I was thinking about the same solution, but, your solution doesn't cover whitespaces. And since Swift 4.2 random number can be just n = Int.random(in: 1...maxValue)
@RobertDresler Good point for the n = Int.random(in: 1...maxValue). Not sure what do you mean by "your solution doesn't cover whitespaces"... Thanks.
my english skills... :D I mean, your solution replaces whitespaces with underscores too. And I think, OP doesn't want this.
@RobertDresler Thank you once again :) Edited.
1

A solution that keeps whitespaces and punctation intact.
We will find them with an extension method indiciesOfPuntationBlanks() -> [Int]. replacing the randomly picked chars will be done by blankOut(percentage: Double) -> String

extension String {
    func indiciesOfPuntationBlanks() -> [Int] {
        let charSet = CharacterSet.punctuationCharacters.union(.whitespaces)
        var indices = [Int]()

        var searchStartIndex = self.startIndex
        while searchStartIndex < self.endIndex,
            let range = self.rangeOfCharacter(from: charSet, options: [], range: searchStartIndex ..< self.endIndex),
            !range.isEmpty
        {
            let index = distance(from: self.startIndex, to: range.lowerBound)
            indices.append(index)
            searchStartIndex = range.upperBound
        }

        return indices
    }


    func blankOut(percentage: Double) -> String {
        var result = self
        let blankIndicies = result.indiciesOfPuntationBlanks()
        let allNonBlankIndicies = Set(0 ..< result.count).subtracting(blankIndicies).shuffled()
        let picked = allNonBlankIndicies.prefix(Int(Double(allNonBlankIndicies.count) * percentage))

        picked.forEach { (idx) in
            let start = result.index(result.startIndex, offsetBy: idx);
            let end = result.index(result.startIndex, offsetBy: idx + 1);
            result.replaceSubrange(start ..< end, with: "_")
        }

        return result
    }
}

Usage:

let str = "Hello, World!"

for _ in 0 ..< 10 {
    print(str.blankOut(percentage: 0.75))
}

Output:

____o, _or__!
_e___, __rl_!
_e__o, __r__!
H____, W_r__!
H_l__, W____!
_____, _or_d!
_e_lo, _____!
_____, _orl_!
_____, _or_d!
___l_, W___d!

Same solution but the string for blanking out and the character sets to be ignored can be configured

extension String {
    func indicies(with charSets:[CharacterSet]) -> [Int] {
        var indices = [Int]()

        let combinedCahrSet: CharacterSet = charSets.reduce(.init()) { $0.union($1) }
        var searchStartIndex = self.startIndex
        while searchStartIndex < self.endIndex,
            let range = self.rangeOfCharacter(from: combinedCahrSet, options: [], range: searchStartIndex ..< self.endIndex),
            !range.isEmpty
        {
            let index = distance(from: self.startIndex, to: range.lowerBound)
            indices.append(index)
            searchStartIndex = range.upperBound
        }

        return indices
    }

    func blankOut(percentage: Double, with blankOutString: String = "_", ignore charSets: [CharacterSet] = [.punctuationCharacters, .whitespaces]) -> String {
        var result = self
        let blankIndicies = result.indicies(with: charSets)
        let allNonBlankIndicies = Set(0 ..< result.count).subtracting(blankIndicies).shuffled()
        let picked = allNonBlankIndicies.prefix(Int(Double(allNonBlankIndicies.count) * percentage))

        picked.forEach { (idx) in
            let start = result.index(result.startIndex, offsetBy: idx);
            let end = result.index(result.startIndex, offsetBy: idx + 1);
            result.replaceSubrange(start ..< end, with: blankOutString)
        }

        return result
    }
}

Usage:

let str = "Hello, World!"

for _ in 0 ..< 10 {
    print(str.blankOut(percentage: 0.75))
}
print("--------------------")

for _ in 0 ..< 10 {
    print(str.blankOut(percentage: 0.75, with:"x", ignore: [.punctuationCharacters]))
}

print("--------------------")

for _ in 0 ..< 10 {
    print(str.blankOut(percentage: 0.75, with:"*", ignore: []))
}

Output:

_el_o, _____!
__llo, _____!
He__o, _____!
_e___, W_r__!
_el_o, _____!
_el__, ___l_!
_e___, __rl_!
_e__o, _o___!
H____, Wo___!
H____, __rl_!
--------------------
xxxlx,xWxrxx!
xxxxx,xxorxd!
Hxxxx,xWxrxx!
xxxxx, xoxlx!
Hxllx,xxxxxx!
xelxx,xxoxxx!
Hxxxx,xWxxxd!
Hxxxo,xxxxxd!
Hxxxx,xxorxx!
Hxxxx, Wxxxx!
--------------------
***l***Wo**d*
*e**o**W**l**
***lo**Wo****
*el*****or***
H****,****ld*
***l*, **r***
*el*o* ******
*e*lo*******!
H*l****W***d*
H****, **r***

2 Comments

sorry I did not downvoted it. you can edit it a little. so I can upvoted it.
@E.Coms: sure, no problem
0

You can use a 3-steps algorithm that does the following:

  1. builds the list of all non-space indices
  2. removes the first 25% random elements from that list
  3. go through all characters and replace all whose index is part of list from #2, by an underscore

The code could look something like this:

func underscorize(_ str: String, factor: Double) -> String {
    // making sure we have a factor between 0 and 1
    let factor = max(0, min(1, factor))
    let nonSpaceIndices = str.enumerated().compactMap { $0.1 == " " ? nil : $0.0 }
    let replaceIndices = nonSpaceIndices.shuffled().dropFirst(Int(Double(str.count) * factor))
    return String(str.enumerated().map { replaceIndices.contains($0.0) ? "_" : $0.1 })
}

let str = "Hello playground"
print(underscorize(str, factor: 0.25))

Sample results:

____o p_ay______
____o p__y____n_
_el_o p_________

Comments

0

The idea is same as above methods, just with a little less code.

var str = "Hello playground"

print(randomString(str))
 print(randomString(str))
// counting whitespace as a random factor
func randomString(_ str: String) -> String{
let strlen = str.count
let effectiveCount = Int(Double(strlen) * 0.25)
let shuffled = (0..<strlen).shuffled()
return String(str.enumerated().map{
      shuffled[$0.0] < effectiveCount || ($0.1) == " " ? ($0.1) : "_"
 })}


//___l_ _l__gr____
//H____ p___g____d


func underscorize(_ str: String) -> String{
let effectiveStrlen  = str.filter{$0 != " "}.count
let effectiveCount = Int(floor(Double(effectiveStrlen) * 0.25))
let shuffled = (0..<effectiveStrlen).shuffled()
return String((str.reduce(into: ([],0)) {
  $0.0.append(shuffled[$0.1] <= effectiveCount || $1 == " "  ?  $1 : "_" )
  $0.1 += ($1 == " ") ? 0 : 1}).0)
 }


 print(underscorize(str))
 print(underscorize(str))

//__l__ pl__g_____
//___lo _l_______d

Comments

0

First you need to get the indices of your string and filter the ones that are letters. Then you can shuffle the result and pick the number of elements (%) minus the number of spaces in the original string, iterate through the result replacing the resulting ranges with the underscore. You can extending RangeReplaceable protocol to be able to use it with substrings as well:


extension StringProtocol where Self: RangeReplaceableCollection{
    mutating func randomReplace(characterSet: CharacterSet = .letters, percentage: Double, with element: Element = "_") {
        precondition(0...1 ~= percentage)
        let indices = self.indices.filter {
            characterSet.contains(self[$0].unicodeScalars.first!)
        }
        let lettersCount = indices.count
        let nonLettersCount = count - lettersCount
        let n = lettersCount - nonLettersCount - Int(Double(lettersCount) * Double(1-percentage))
        indices
            .shuffled()
            .prefix(n)
            .forEach {
                replaceSubrange($0...$0, with: Self([element]))
        }
    }
    func randomReplacing(characterSet: CharacterSet = .letters, percentage: Double, with element: Element = "_") -> Self {
        precondition(0...1 ~= percentage)
        var result = self
        result.randomReplace(characterSet: characterSet, percentage: percentage, with: element)
        return result
    }
}

// mutating test
var str = "Hello playground"
str.randomReplace(percentage: 0.75)     // "___lo _l___r____\n"
print(str)                              // "___lo _l___r____\n"

// non mutating with another character
let str2 = "Hello playground"
str2.randomReplacing(percentage: 0.75, with: "•")  // "••••o p••y•••u••"
print(str2) //  "Hello playground\n"

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.