CouponCodeEx v0.1.0 CouponCode View Source

A module for generating coupon codes in Elixir. Inspired by the Perl library CouponCode, a coupon code is a group of letters and numbers known as part and separated by a hyphen(-) that is meant for a receipient types in. For example, a 3 part code with 4 characters per part looks like this:

  H6YV-UDPL-383N

Like with the original module, it has the same features:

  • Codes are validated regardless of case (upper or lower)

  • Codes use the upper cased letters and numbers; however, it does not use the letters "O", "I", "Z" and "S" since they are visually similar to "0", "1", "2" and "5". Still, the receipient can enter those ambigious letters and be considered valid or corrected.

  • The last character of every part is a checkdigit that helps in determining which specifc part(s) has been entered correctly.

  • Generated parts are also rejected against a list of bad words since they are manually entered.

  • Generated parts that can be transposed (or for example with a 4 character part ABCD, the parts BACD. ACBD and ABDC generate a valid checkdigit) are also rejected.

Config

While this library has good defaults, it can be configured like so:

  config :coupon_code_ex,
    parts: 3
    part_length: 4
    bad_words: ~w(SHPX PHAG JNAX JNAT CVFF PBPX FUVG GJNG GVGF SNEG URYY ZHSS QVPX XABO NEFR FUNT GBFF FYHG GHEQ FYNT PENC CBBC OHGG SRPX OBBO WVFZ WVMM CUNG)

While part_length can have multiple values, it is discouraged to change it from the default since the bad_words is built for that length. If you still want to change it, remember to update the bad_words lists to the maximum length that is supported.

When customizing the bad_words list, remember it is encoded with rot13 to avoid profanity in the code or configuraton. So right before adding a new word to that list, you can encode it with CouponCode.rot13/1 or an online rot13 encoder.

Link to this section Summary

Functions

Generate the regex used in detecting bad words from the generated codes.

Generate a random coupon code.

rot13 utility function to encode/decode bad words.

Validates a receipient entered code based on the required parts and length it should have.

Link to this section Functions

Link to this function

bad_word_regex()

View Source
bad_word_regex() :: Regex.t()

Generate the regex used in detecting bad words from the generated codes.

Aside from detecting all words in the decoded list, it should detect similar characters (0 to O, 1 to I, 2 to S and 5 to Z).

Examples

  iex> "P00P" =~ CouponCode.bad_word_regex()
  true

  iex> "POOP" =~ CouponCode.bad_word_regex()
  true

  iex> "P00P1E" =~ CouponCode.bad_word_regex()
  false

  iex> "F0RD" =~ CouponCode.bad_word_regex()
  false
Link to this function

generate(opts \\ [])

View Source
generate(Keyword.t()) :: charlist()

Generate a random coupon code.

Algorithm

Like with the original module, each generated code uses a plaintext as a source of bytes. It is then hashed with sha1 and consumed for each random character that is needed (which excludes checkdigits) to generate a part. When the bytes are insufficient to generate a new part or is rejected by having a filtered word or is transposable, it is rehashed to generate and used as the new source of bytes. This process is repeated until every part is generated.

Options

This function takes a keyword options as the first argument:

  • plaintext - The plaintext to use for generating the coupon code. Useful in generating the same or deterministic code with the same options but usually not filled in. If none is given, a random 8 byte plaintext is generated.

  • parts - The number of delimited segments to generate. Defaults to 3 or Application.get_env(:coupon_code_ex, :parts) and must be a positive integer.

  • part_length - The number of characters per each part which includes the checkdigit. Defaults to 4 or Application.get_env(:coupon_code_ex, :part_length) and must be a positive integer between 2 - 20 inclusively. (The limitation stems from sha1 generating exactly 20 bytes.)

Examples

  iex> CouponCode.generate(plaintext: "1234567890")
  "1K7Q-CTFM-LMTC"

  iex> CouponCode.generate(plaintext: "123456789A")
  "X730-KCV1-MA2G"

  iex> CouponCode.generate()
  "5UMN-WBKJ-2MCA"

  iex> CouponCode.generate(parts: 1)
  "YUVN"

  iex> CouponCode.generate(parts: 5)
  "D51P-H52K-9VMD-UT5H-XE3A"

  iex> CouponCode.generate(part_length: 3)
  "GVB-KDB-ADF"

  iex> CouponCode.generate(part: 2, part_length: 7)
  "86NMUDX-GFEJHVR"

rot13 utility function to encode/decode bad words.

This encoding is a character substitution algorithm by rotating a letter 13 places after it. Lower case characters are equally converted while anything else is passedthrough.

Examples

  iex> CouponCode.rot13("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
  "NOPQRSTUVWXYZABCDEFGHIJKLM"

  iex> CouponCode.rot13("abcdefghijklmnopqrstuvwxyz")
  "nopqrstuvwxyzabcdefghijklm"

  iex> CouponCode.rot13("1234567890!@#$%^&*()")
  "1234567890!@#$%^&*()"

  iex> CouponCode.rot13("Hello World!")
  "Uryyb Jbeyq!"
Link to this function

validate(code, opts \\ [])

View Source
validate(charlist(), Keyword.t()) ::
  {:ok, charlist()}
  | {:error, :parts_invalid, pos_integer()}
  | {:error, :part_invalid, pos_integer()}

Validates a receipient entered code based on the required parts and length it should have.

With an entered code, it is normalized with the following steps:

  • It is uppercased
  • All non-word (0-9A-Z) characters are removed
  • Similar letters (OIZS) are corrected (0125)

If the normalized code is valid, this returns {:ok, corrected_code}. If the number of computed parts are invalid, it returns {:error, :parts_invalid, actual_parts}. Lastly it returns {:error, :part_invalid, part_with_error} for the first parsed part that has an checkdigit error.

Options

This function takes a keyword options as the second argument:

  • parts - The number of expected delimited segments to generate. Defaults to 3 or Application.get_env(:coupon_code_ex, :parts) and must be a positive integer.

  • part_length - The number of expected characters per each part. Defaults to 4 or Application.get_env(:coupon_code_ex, :part_length) and must be a positive integer between 2 - 20 inclusively.

Examples

  iex> CouponCode.validate("7B5M-LJ4J-D5FN")
  {:ok, "7B5M-LJ4J-D5FN"}

  iex> CouponCode.validate("7B5MLJ4JD5FN")
  {:ok, "7B5M-LJ4J-D5FN"}

  iex> CouponCode.validate("7B5mlJ4jd5fn")
  {:ok, "7B5M-LJ4J-D5FN"}

  iex> CouponCode.validate("7B5mlJ4jd5fn", parts: 4)
  {:error, :parts_invalid, 3}

  iex> CouponCode.validate("7B5mlJ4jd5fn", part_length: 5)
  {:error, :parts_invalid, 2}

  iex> CouponCode.validate("7B5mlJ4jd5fM")
  {:error, :part_invalid, 2}

  iex> CouponCode.validate("i9oD-V467-8Dsz")
  {:ok, "190D-V467-8D52"}