duckling/Duckling/CreditCardNumber/Helpers.hs
Julien Odent bf89e34365 Relicense to BSD3
Reviewed By: JoelMarcey

Differential Revision: D15439223

fbshipit-source-id: c5af3cb06318748142fe503945b38beffadfc28a
2019-05-22 10:46:39 -07:00

124 lines
4.0 KiB
Haskell

-- Copyright (c) 2016-present, Facebook, Inc.
-- All rights reserved.
--
-- This source code is licensed under the BSD-style license found in the
-- LICENSE file in the root directory of this source tree.
module Duckling.CreditCardNumber.Helpers
( otherCreditCardNumberRegex
, visaCreditCardNumberRegex
, amexCreditCardNumberRegex
, discoverCreditCardNumberRegex
, mastercardCreditCardNumberRegex
, dinerClubCreditCardNumberRegex
, isValidCreditCardNumber
, minNumberDigits
, maxNumberDigits
, creditCard
) where
import Data.Text (Text)
import Prelude
import Data.String
import Duckling.CreditCardNumber.Types (CreditCardNumberData(..))
import qualified Duckling.CreditCardNumber.Types as TCreditCardNumber
import qualified Data.Text as T
import qualified Data.Char as C
import qualified Data.Bits as B
-- -----------------------------------------------------------------
-- Patterns
otherCreditCardNumberRegex :: String
otherCreditCardNumberRegex =
concat [ "("
, "(?!" , visaCreditCardNumberRegex, ")"
, "(?!" , amexCreditCardNumberRegex, ")"
, "(?!" , discoverCreditCardNumberRegex, ")"
, "(?!" , mastercardCreditCardNumberRegex, ")"
, "(?!" , dinerClubCreditCardNumberRegex, ")"
, "\\d{" , show minNumberDigits , "," , show maxNumberDigits , "}"
, ")"
]
-- | Visa credit card regex informed by latest BIN info
visaCreditCardNumberRegex :: String
visaCreditCardNumberRegex = "(" ++ withoutDashes ++ "|" ++ withDashes ++")"
where
withoutDashes = "4[0-9]{15}"
withDashes = "4[0-9]{3}-[0-9]{4}-[0-9]{4}-[0-9]{4}"
-- | American Express credit card regex informed by latest BIN info
amexCreditCardNumberRegex :: String
amexCreditCardNumberRegex = "(" ++ withoutDashes ++ "|" ++ withDashes ++")"
where
withoutDashes = "3[47][0-9]{13}"
withDashes = "3[47][0-9]{2}-[0-9]{6}-[0-9]{5}"
-- | Discover credit card regex informed by latest BIN info
discoverCreditCardNumberRegex :: String
discoverCreditCardNumberRegex = "(" ++ withoutDashes ++ "|" ++ withDashes ++")"
where
withoutDashes = "6(?:011|[45][0-9]{2})[0-9]{12}"
withDashes = "6(?:011|[45][0-9]{2})-[0-9]{4}-[0-9]{4}-[0-9]{4}"
-- | Mastercard credit card regex informed by latest BIN info
mastercardCreditCardNumberRegex :: String
mastercardCreditCardNumberRegex =
"(" ++ withoutDashes ++ "|" ++ withDashes ++")"
where
withoutDashes = "5[1-5][0-9]{14}"
withDashes = "5[1-5][0-9]{2}-[0-9]{4}-[0-9]{4}-[0-9]{4}"
-- | Diner Club credit card regex informed by latest BIN info
dinerClubCreditCardNumberRegex :: String
dinerClubCreditCardNumberRegex = "(" ++ withoutDashes ++ "|" ++ withDashes ++")"
where
withoutDashes = "3(?:0[0-5]|[68][0-9])[0-9]{11}"
withDashes = "3(?:0[0-5]|[68][0-9])[0-9]-[0-9]{6}-[0-9]{4}"
-- -----------------------------------------------------------------
-- Validation
-- | An implementation of the Luhn algorithm (see
-- https://en.wikipedia.org/wiki/Luhn_algorithm) to check if a given credit card
-- number is valid
isValidCreditCardNumber :: Text -> Bool
isValidCreditCardNumber ccNum =
T.length ccNum >= minNumberDigits &&
T.length ccNum <= maxNumberDigits &&
validCheckSum
where
validCheckSum :: Bool
validCheckSum =
T.all C.isDigit ccNum &&
fst (T.foldr f (0, 0) ccNum) `rem` 10 == 0
where
f char (checksum, e) =
let
val = C.digitToInt char
-- every even digit should be doubled
d = sumDigits (B.shift val e)
in (checksum + d, 1 - e)
-- we only need sum of digits for numbers from 0 to 18
sumDigits a
| a > 9 = a - 9
| otherwise = a
minNumberDigits :: Int
minNumberDigits = 8
maxNumberDigits :: Int
maxNumberDigits = 19
-- -----------------------------------------------------------------
-- Production
creditCard :: Text -> TCreditCardNumber.Issuer -> CreditCardNumberData
creditCard ccNum i =
CreditCardNumberData { TCreditCardNumber.number = ccNum
, TCreditCardNumber.issuer = i
}