Random replace using Swift

2020-07-24 04:30发布

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?

9条回答
手持菜刀,她持情操
2楼-- · 2020-07-24 05:07

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
查看更多
Rolldiameter
3楼-- · 2020-07-24 05:11

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_________
查看更多
等我变得足够好
4楼-- · 2020-07-24 05:14

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"
查看更多
甜甜的少女心
5楼-- · 2020-07-24 05:15

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_
查看更多
祖国的老花朵
6楼-- · 2020-07-24 05:18

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
查看更多
叛逆
7楼-- · 2020-07-24 05:20

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.

查看更多
登录 后发表回答