How would i generate a random and unique string in Ruby? - ruby-on-rails

In a Ruby on Rails app I am working on I allow users to upload files and want to give these files a short, random alphanumeric name. (Eg 'g7jf8' or '3bp76'). What is the best way to do this?
I sas thinking of generating a hash / encrypted string from the original filename and timestamp. Then query the database to double check it doesnt exist. If it does, generate another and repeat.
The issue i see with this approach is if there is high propability of duplicate strings, it could add quite a lote of datbase load.

I use this :)
def generate_token(column, length = 64)
begin
self[column] = SecureRandom.urlsafe_base64 length
end while Model.exists?(column => self[column])
end
Replace Model by your model name

SecureRandom.uuid
Will give you a globally unique String. http://en.m.wikipedia.org/wiki/Universally_unique_identifier
SecureRandom.hex 32
Will give a random String, but it's algorithm is not optimised for uniqueness. Of course the chance of collision with 32 digits, assuming true randomness, is basically theoretical. You could make 1 billion per second for 100 years and have only a 50% chance of a collision.

Use Ruby's SecureRandom.hex function with optional number of character you wanted to generate.

This will always produce new uniq 40 size alpha-numeric string, because it has Time stamp also.
loop do
random_token = Digest::SHA1.hexdigest([Time.now, rand(111..999)].join)
break random_token unless Model.exists?(column_name: random_token)
end
Note: Replace Model by your model_name and column_name by any existing column of your model.

You can assign a unique id by incrementing it each time a new file is added, and convert that id into an encrypted string using OpenSSL::Cipher with a constant key that you save somewhere.

If you end up generating a hex or numeric digest, you can keep the code shorter by representing the number as e.g. Base 62:
# This is a lightweight base62 encoding for Ruby integers.
B62CHARS = ('0'..'9').to_a + ('a'..'z').to_a + ('A'..'Z').to_a
def base62_string nbr
b62 = ''
while nbr > 0
b62 << B62CHARS[nbr % 62]
nbr /= 62
end
b62.reverse
end
If it is important for you to restrict the character set used (for instance not have uppercase chars in file names), then this code can easily be adapted, provided you can find a way of feeding in a suitable random number.
If your file names are supposed to be semi-secure, you need to arrange that there are many more possible names than actual names in storage.

It looks like you actually need a unique filenames, right? Why not forget about complex solutions and simply use Time#nsec?
t = Time.now #=> 2007-11-17 15:18:03 +0900
"%10.9f" % t.to_f #=> "1195280283.536151409"

You can use Time in miliseconds and than convert it into base 36 to reduce it length.
and since it depends on time to it will be very unique.
Example:
Time.now.to_f.to_s.gsub('.', '').ljust(17, '0').to_i.to_s(36) # => "4j26lna7g62"
Take a look at this answer:
https://stackoverflow.com/a/72738840/7365329

Related

Ruby: how to generate unique alphabetic string in ruby

Is there any in built method in ruby which will generate unique alphabetic string every time(it should not have numbers only alphabets)?
i have tried SecureRandom but it doesn't provide method which will return string containing only alphabets.
SecureRandom has a method choose which:
[...] generates a string that randomly draws from a source array of characters.
Unfortunately it's private, but you can call it via send:
SecureRandom.send(:choose, [*'a'..'z'], 8)
#=> "nupvjhjw"
You could also monkey-patch Random::Formatter:
module Random::Formatter
def alphabetic(n = 16)
choose([*'a'..'z'], n)
end
end
SecureRandom.alphabetic
#=> "qfwkgsnzfmyogyya"
Note that the result is totally random and therefore not necessarily unique.
UUID are designed to have extremely low chance of collision. Since UUID only uses 17 characters, it's easy to change the non-alphabetic characters into unused alphabetic slots.
SecureRandom.uuid.gsub(/[\d-]/) { |x| (x.ord + 58).chr }
Is there any in built method in ruby which will generate unique alphabetic string every time(it should not have numbers only alphabets)?
This is not possible. The only way that a string can be unique if you are generating an unlimited number of strings is if the string is infinitely long.
So, it is impossible to generate a string that will be unique every time.
def get_random_string(length=2)
source=("a".."z").to_a + ("A".."Z").to_a
key=""
length.times{ key += source[rand(source.size)].to_s }
key
end
How about something like this if you like some monkey-patching, i have set length 2 here , please feel free to change it as per your needs
get_random_string(7)
I used Time in miliseconds, than converted it into base 36 which gives me unique aplhanumeric value and since it depends on time so, it will be very unique.
Example: Time.now.to_f.to_s.gsub('.', '').ljust(17, '0').to_i.to_s(36) # => "4j26m4zm2ss"
Take a look at this for full answer: https://stackoverflow.com/a/72738840/7365329
Try this one
length = 50
Array.new(length) { [*"A".."Z", *"a".."z"].sample }.join
# => bDKvNSySuKomcaDiAlTeOzwLyqagvtjeUkDBKPnUpYEpZUnMGF

Specifying minimum length when using FFaker::Internet.user_name

I have a spec that keeps failing because FFaker::Internet.user_name generates a word that is less than 5 characters.
How do I specify a minimum length in this stmt:
username { FFaker::Internet.user_name }
String#ljust — Returns a copy of self of a given length, right-padded with a given other string.
username { FFaker::Internet.user_name.ljust(5,"12345") }
From what I see you try to use FFaker in your factory. Why overcomplicate things for your specs, when you could define sequence
sequence(:username) do |n|
"username-#{n}"
end
But the question is valid and you may have some legitimate needs to use ffaker, and there are many ways to do it. You can just concatenate username twice, why not?
username { FFaker.username + FFaker.username }
Or keep looking for a username that length is of minimal lenght:
username do
do
name = FFaker.username
while name.length < 5
name
end
Or monkeypatch ffaker and implement it yourself https://github.com/ffaker/ffaker/blob/0578f7fd31c9b485e9c6fa15f25b3eca724790fe/lib/ffaker/internet.rb#L43 + https://github.com/ffaker/ffaker/blob/0578f7fd31c9b485e9c6fa15f25b3eca724790fe/lib/ffaker/name.rb#L75
for example
class FFaker
def long_username(min_length = 5)
fetch_sample(FIRST_NAMES.select {|name| name.length >= min_lenght })
end
end
There're many ways you can achieve this, but if I had to do it, I'd do something like
(FFaker::Internet.user_name + '___')[0...5]
#=> "Lily_"
There are three underscores because after the quick lookup to the name list, I found the minimum length of first name is two characters so two plus three will always be at least five characters.
I'm only taking five character substring so as to not always have trailing underscore, but that's just my personal preference, you can use username plus three underscores and your test case will do fine.
You can't, but you could do FFaker::Name.name.join, this generates first name and middle name
You can also build it manually with regex using FFaker::String like so:
# 5 characters username, with first character being a letter
username { FFaker::String.from_regexp(/[a-zA-Z]\w{4}/) }
# random 5-10 characters username with first character being a letter
regex = Regexp.new("[a-zA-Z]\\w{#{Random.rand(4..9)}}")
username { FFaker::String.from_regexp(regex) }
Another idea is to add some random numbers to the end. This worked for me, was easy to implement, and looks fairly natural (since humans tend to do it when creating usernames in the wild).
E.g. this adds "1234" to the end of usernames:
"steve" + "1234"
and with Faker:
Faker::Internet.unique.user_name + "1234"
Note: if you want a random string (instead of "1234", try some of these approaches. However, it may not be necessary if you're already using the Faker .unique method as in the above example.

Regex password validation for non-sequence data

I'm attempting to validate that my passwords may not contain any sequential characters such as "123" or "abcd" etc. I'm new to regex and i'm trying to do it by having the following code in my User model:
validate :password_complexity
def password_complexity
if password.present? and not password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d). /)
errors.add :password, "must include at least one lowercase letter, one uppercase letter, and one digit"
end
end
This is probably somewhat outside the bounds of regex. I would recommend looking into zxcvbn, which has also been ported into a ruby gem of the same name and even exists as a devise plugin. This will provide you with significantly more robust verification. You may also consider just inspecting the code for these projects, for inspiration, if you are determined to learn how one might implement something like this from scratch.
I agree that this isn't a job for a regex: maybe you could do it but it would be very complicated. Here's how i would do it.
def contains_sequential_characters?(string)
sequential = false
string.split("").each_with_index do |char,i|
if (string[i+1] == string[i]+1) && char =~ /[a-y0-8]/i
sequential = true
end
end
sequential
end
At the heart of this solution is that calling an array index on a string (eg "foo"[2]) will give you the ascii code of the character at that position. We only care about ascii codes that are next to each other between a-z and 0-9, which is why i'm also matching the first character against a-y (not z) and 0-8 (not 9).
You can use the below regex, which will accept a minimum of 13 digits and will not accept either sequential digits or sequential characters:
(?=.*?[0-9])(?!.*((12)|(23)|(34)|(45)|(56)|(67)|(78)|(90)|(01)))(?=.*?[a-zäöüß])(?!.*((ab)|(bc)|(cd)|(ef)|(fg)|(gh)|(hi)|(ij)|(jk)|(kl)|(lm)|(mn)|(no)|(op)|(pq)|(qr)|(rs)|(st)|(tu)|(uv)|(vw)|(wx)|(xy)|(yz)))(?=.*?[A-ZÄÖÜ])(?!.*((AB)|(BC)|(CD)|(EF)|(FG)|(GH)|(HI)|(IJ)|(JK)|(KL)|(LM)|(MN)|(NO)|(OP)|(PQ)|(QR)|(RS)|(ST)|(TU)|(UV)|(VW)|(WX)|(xy)|(yz))).{13,}

SHA1 hashing in Rails

I have database with following fields: id, q_id, text, session etc. and I have already 2 records there. I want to hash each line with SHA1 alghoritm (every line is unique of course). I tried this:
#w = Digest::SHA1.hexdigest(id+q_id+text+session)
but it doesn't work.
SHA is not encryption but rather it creates a cryptographic hash. If that is still what you want to do, my guess is that id and q_id are Fixnums and needs to be converted to strings.
#w = Digest::SHA1.hexdigest(ans.id.to_s + ans.q_id.to_s + ans.text + ans.session)
I also kind of like to use String literals because it makes it very obvious that we are dealing with a string
#w = Digest::SHA1.hexdigest("#{id}#{q_id}#{text}#{session}")

Compressing a hex string in Ruby/Rails

I'm using MongoDB as a backend for a Rails app I'm building. Mongo, by default, generates 24-character hexadecimal ids for its records to make sharding easier, so my URLs wind up looking like:
example.com/companies/4b3fc1400de0690bf2000001/employees/4b3ea6e30de0691552000001
Which is not very pretty. I'd like to stick to the Rails url conventions, but also leave these ids as they are in the database. I think a happy compromise would be to compress these hex ids to shorter collections using more characters, so they'd look something like:
example.com/companies/3ewqkvr5nj/employees/9srbsjlb2r
Then in my controller I'd reverse the compression, get the original hex id and use that to look up the record.
My question is, what's the best way to convert these ids back and forth? I'd of course want them to be as short as possible, but also url-safe and simple to convert.
Thanks!
You could represent a hexadecimal id in a base higher than 16 to make its string representation shorter. Ruby has built-in support for working with bases from 2 up to 36.
b36 = '4b3fc1400de0690bf2000001'.hex.to_s(36)
# => "29a6dblglcujcoeboqp"
To convert it back to a 24-character string you could do something like this:
'%024x' % b36.to_i(36)
# => "4b3fc1400de0690bf2000001"
To achieve better "compression" you could represent the id in base higher than 36. There are Ruby libraries that will help you with that. all-your-base gem is one such library.
I recommend base 62 representation as it only uses 0-9, a-z and A-Z characters which means it is URL safe by default.
Even with base 62 representation you end up with still unwieldy 16-character ids:
'4b3fc1400de0690bf2000001'.hex.to_base_62
# => "UHpdfMzq7jKLcvyr"
Sidestepping Rails convention a bit, another compromise is to use as the "URL id" the base 32 representation of the created_at date of the object.
aCompany.created_at
# => Sat Aug 13 20:05:35 -0500 2011
aCompany.created_at.to_i.to_s(32)
# => "174e7qv"
This way you get super short ids (7 characters) without having to keep track of a special purpose attribute (in MongoMapper, it's a simple matter of adding timestamps! in the model to get automatic created_at and updated_at attributes).
You can use base64 to make it shorter. Make sure that you are using '-' and '_' instead of '+' and '/'. You can also chop of the padding =.
Code to convert from a hex value to base 64
def MD5hex2base64(str)
h1=[].clear
# split the 32 byte hex into a 16 byte array
16.times{ h1.push(str.slice!(0,2).hex) }
# pack (C* = unsigned char), (m = base64 encoded output)
[h1.pack("C*")].pack("m")
end

Resources