diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Allen Nelson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/semver-range.cabal b/semver-range.cabal
new file mode 100644
--- /dev/null
+++ b/semver-range.cabal
@@ -0,0 +1,37 @@
+name:                semver-range
+version:             0.1.0
+synopsis:            An implementation of semver and semantic version ranges.
+license:             MIT
+license-file:        LICENSE
+author:              Allen Nelson
+maintainer:          anelson@narrativescience.com
+build-type:          Simple
+cabal-version:       >=1.10
+bug-reports:         https://github.com/adnelson/semver-range/issues
+
+source-repository head
+  type:     git
+  location: git://github.com/adnelson/semver-range.git
+
+library
+  Exposed-modules:
+    Data.SemVer
+    Data.SemVer.Parser
+  other-extensions:    FlexibleContexts
+                     , FlexibleInstances
+                     , LambdaCase
+                     , NoImplicitPrelude
+                     , NoMonomorphismRestriction
+                     , OverloadedStrings
+                     , QuasiQuotes
+                     , RecordWildCards
+                     , ScopedTypeVariables
+                     , TypeFamilies
+                     , TypeSynonymInstances
+  build-depends:       base >=4.8 && <4.9
+                     , classy-prelude
+                     , text
+                     , unordered-containers
+                     , parsec
+  hs-source-dirs:      src
+  default-language:    Haskell2010
diff --git a/src/Data/SemVer.hs b/src/Data/SemVer.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/SemVer.hs
@@ -0,0 +1,190 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE LambdaCase #-}
+module Data.SemVer where
+
+import ClassyPrelude
+import qualified Prelude as P
+import Data.Text (Text)
+import qualified Data.Text as T
+
+type ReleaseTag = Text
+
+-- | A SemVer has major, minor and patch versions, and zero or more
+-- pre-release version tags.
+type SemVer = (Int, Int, Int, [ReleaseTag])
+
+-- | A partially specified semantic version. Implicitly defines
+-- a range of acceptable versions, as seen in @wildcardToRange@.
+data Wildcard = Any
+              | One Int
+              | Two Int Int
+              | Three Int Int Int [Text]
+              deriving (Show, Eq)
+
+-- | A range specifies bounds on a semver.
+data SemVerRange
+  = Eq SemVer                   -- ^ Exact equality
+  | Gt SemVer                   -- ^ Greater than
+  | Lt SemVer                   -- ^ Less than
+  | Geq SemVer                  -- ^ Greater than or equal to
+  | Leq SemVer                  -- ^ Less than or equal to
+  | And SemVerRange SemVerRange -- ^ Conjunction
+  | Or SemVerRange SemVerRange  -- ^ Disjunction
+  deriving (Eq, Ord)
+
+-- | Pull all of the concrete versions out of a range.
+versionsOf :: SemVerRange -> [SemVer]
+versionsOf = \case
+  Eq sv -> [sv]
+  Geq sv -> [sv]
+  Leq sv -> [sv]
+  Lt sv -> [sv]
+  Gt sv -> [sv]
+  And svr1 svr2 -> versionsOf svr1 <> versionsOf svr2
+  Or svr1 svr2 -> versionsOf svr1 <> versionsOf svr2
+
+-- | Create a SemVer with no version tags.
+semver :: Int -> Int -> Int -> SemVer
+semver a b c = (a, b, c, [])
+
+-- | Get the release tags from a semver.
+releaseTags :: SemVer -> [ReleaseTag]
+releaseTags (_, _, _, tags) = tags
+
+-- | Get only the version tuple from a semver.
+toTuple :: SemVer -> (Int, Int, Int)
+toTuple (a, b, c, _) = (a, b, c)
+
+-- | Get a list of tuples from a version range.
+tuplesOf :: SemVerRange -> [(Int, Int, Int)]
+tuplesOf = map toTuple . versionsOf
+
+-- | Get all of the release tags from a version range.
+rangeReleaseTags :: SemVerRange -> [ReleaseTag]
+rangeReleaseTags = concatMap releaseTags . versionsOf
+
+-- | Satisfies any version.
+anyVersion :: SemVerRange
+anyVersion = Gt $ semver 0 0 0
+
+-- | Render a semver as Text.
+renderSV :: SemVer -> Text
+renderSV = pack . renderSV'
+
+-- | Render a semver as a String.
+renderSV' :: SemVer -> String
+renderSV' (x, y, z, []) = show x <> "." <> show y <> "." <> show z
+renderSV' (x, y, z, tags) = renderSV' (x, y, z, []) <> "-" <>
+                              (intercalate "." $ map unpack tags)
+
+instance Show SemVerRange where
+  show = \case
+    Eq sv -> "=" <> renderSV' sv
+    Gt sv -> ">" <> renderSV' sv
+    Lt sv -> "<" <> renderSV' sv
+    Geq sv -> ">=" <> renderSV' sv
+    Leq sv -> "<=" <> renderSV' sv
+    And svr1 svr2 -> show svr1 <> " " <> show svr2
+    Or svr1 svr2 -> show svr1 <> " || " <> show svr2
+
+-- | Returns whether a given semantic version matches a range.
+-- Note that there are special cases when there are release tags. For detauls
+-- see https://github.com/npm/node-semver#prerelease-tags.
+matches :: SemVerRange -> SemVer -> Bool
+matches range ver = case (rangeReleaseTags range, releaseTags ver) of
+  -- This is the simple case, where neither the range nor the version has given
+  -- any release tags. Then we can just do regular predicate calculus.
+  ([], []) -> matchesSimple range ver
+  -- If the version has release tags but the range doesn't, it's not a match.
+  ([], _) -> False
+  -- If the version has release tags but none of the comparators have the same
+  -- (major, minor, patch) tuples, it's not a match. For now, we're being even
+  -- more strict, saying that there has to be exactly one comparator as well.
+  (_, _) | tuplesOf range /= [toTuple ver] -> False
+  -- If they have the same tuples, we can compare the version tags.
+  (rTags, vTags) -> matchesTags range rTags vTags
+
+-- | Simple predicate calculus matching, doing AND and OR combination with
+-- numerical comparison.
+matchesSimple :: SemVerRange -> SemVer -> Bool
+matchesSimple range ver = case range of
+  Eq sv -> ver == sv
+  Gt sv -> ver > sv
+  Lt sv -> ver < sv
+  Geq sv -> ver >= sv
+  Leq sv -> ver <= sv
+  And sv1 sv2 -> matchesSimple sv1 ver && matchesSimple sv2 ver
+  Or sv1 sv2 -> matchesSimple sv1 ver || matchesSimple sv2 ver
+
+-- | Given a range and two sets of tags, the first being a bound on the second,
+-- uses the range to compare the tags and see if they match.
+matchesTags :: SemVerRange -> [ReleaseTag] -> [ReleaseTag] -> Bool
+matchesTags range rangeTags verTags = case range of
+  Eq _ -> verTags == rangeTags
+  Gt _ -> verTags > rangeTags
+  Lt _ -> verTags < rangeTags
+  Geq _ -> verTags >= rangeTags
+  Leq _ -> verTags <= rangeTags
+  -- Note that as we're currently doing things, these cases won't get hit.
+  And svr1 svr2 -> matchesTags svr1 verTags rangeTags &&
+                     matchesTags svr2 verTags rangeTags
+  Or svr1 svr2 -> matchesTags svr1 verTags rangeTags ||
+                     matchesTags svr2 verTags rangeTags
+
+-- | Gets the highest-matching semver in a range.
+bestMatch :: SemVerRange -> [SemVer] -> Either String SemVer
+bestMatch range vs = case filter (matches range) vs of
+  [] -> Left "No matching versions"
+  vs -> Right $ P.maximum vs
+
+-- | Fills in zeros in a wildcard.
+wildcardToSemver :: Wildcard -> SemVer
+wildcardToSemver Any = (0, 0, 0, [])
+wildcardToSemver (One n) = (n, 0, 0, [])
+wildcardToSemver (Two n m) = (n, m, 0, [])
+wildcardToSemver (Three n m o tags) = (n, m, o, tags)
+
+-- | Translates a wildcard (partially specified version) to a range.
+-- Ex: 2 := >=2.0.0 <3.0.0
+-- Ex: 1.2.x := 1.2 := >=1.2.0 <1.3.0
+wildcardToRange :: Wildcard -> SemVerRange
+wildcardToRange = \case
+  Any -> Geq (0, 0, 0, [])
+  One n -> Geq (n, 0, 0, []) `And` Lt (n+1, 0, 0, [])
+  Two n m -> Geq (n, m, 0, []) `And` Lt (n, m + 1, 0, [])
+  Three n m o tags -> Eq (n, m, o, tags)
+
+-- | Translates a ~wildcard to a range.
+-- Ex: ~1.2.3 := >=1.2.3 <1.(2+1).0 := >=1.2.3 <1.3.0
+tildeToRange :: Wildcard -> SemVerRange
+tildeToRange = \case
+  Any -> tildeToRange (Three 0 0 0 [])
+  One n -> tildeToRange (Three n 0 0 [])
+  Two n m -> tildeToRange (Three n m 0 [])
+  Three n m o tags -> And (Geq (n, m, o, tags)) (Lt (n, m + 1, 0, tags))
+
+-- | Translates a ^wildcard to a range.
+-- Ex: ^1.2.x := >=1.2.0 <2.0.0
+caratToRange :: Wildcard -> SemVerRange
+caratToRange = \case
+  One n -> And (Geq (n, 0, 0, [])) (Lt (n+1, 0, 0, []))
+  Two n m -> And (Geq (n, m, 0, [])) (Lt (n+1, 0, 0, []))
+  Three 0 0 n tags -> Eq (0, 0, n, tags)
+  Three 0 n m tags -> And (Geq (0, n, m, tags)) (Lt (0, n + 1, 0, tags))
+  Three n m o tags -> And (Geq (n, m, o, tags)) (Lt (n+1, 0, 0, tags))
+
+-- | Translates two hyphenated wildcards to an actual range.
+-- Ex: 1.2.3 - 2.3.4 := >=1.2.3 <=2.3.4
+-- Ex: 1.2 - 2.3.4 := >=1.2.0 <=2.3.4
+-- Ex: 1.2.3 - 2 := >=1.2.3 <3.0.0
+hyphenatedRange :: Wildcard -> Wildcard -> SemVerRange
+hyphenatedRange wc1 wc2 = And sv1 sv2 where
+  sv1 = case wc1 of Any -> Geq (0, 0, 0, [])
+                    One n -> Geq (n, 0, 0, [])
+                    Two n m -> Geq (n, m, 0, [])
+                    Three n m o tags -> Geq (n, m, o, tags)
+  sv2 = case wc2 of Any -> Geq (0, 0, 0, []) -- Refers to "any version"
+                    One n -> Lt (n+1, 0, 0, [])
+                    Two n m -> Lt (n, m + 1, 0, [])
+                    Three n m o tags -> Leq (n, m, o, tags)
diff --git a/src/Data/SemVer/Parser.hs b/src/Data/SemVer/Parser.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/SemVer/Parser.hs
@@ -0,0 +1,145 @@
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE LambdaCase #-}
+module Data.SemVer.Parser (
+    parseSemVer, parseSemVerRange, pSemVerRange, pSemVer,
+    fromHaskellVersion
+  ) where
+
+import qualified Prelude as P
+import ClassyPrelude hiding (try)
+import Text.Parsec hiding ((<|>), spaces, parse, State, uncons)
+import qualified Text.Parsec as Parsec
+
+import Data.Version (Version(..))
+import Data.SemVer
+
+type Parser = ParsecT String () Identity
+
+-- | Given a parser and a string, attempts to parse the string.
+parse :: Parser a -> Text -> Either ParseError a
+parse p = Parsec.parse p "" . unpack
+
+parseFull :: Parser a -> Text -> Either ParseError a
+parseFull p = Parsec.parse (p <* eof) "" . unpack
+
+-- | Consumes any spaces (not other whitespace).
+spaces :: Parser String
+spaces = many $ char ' '
+
+-- | Consumes at least one space (not other whitespace).
+spaces1 :: Parser String
+spaces1 = many1 $ char ' '
+
+-- | Parses the given string and any trailing spaces.
+sstring :: String -> Parser String
+sstring = lexeme . string
+
+-- | Parses the given character and any trailing spaces.
+schar :: Char -> Parser Char
+schar = lexeme . char
+
+-- | Parses `p` and any trailing spaces.
+lexeme :: Parser a -> Parser a
+lexeme p = p <* spaces
+
+-- | Parses an integer.
+pInt :: Parser Int
+pInt = lexeme pInt'
+
+-- | Parses an integer without consuming trailing spaces.
+pInt' :: Parser Int
+pInt' = P.read <$> many1 digit
+
+-- | Parse a string as a version range, or return an error.
+parseSemVerRange :: Text -> Either ParseError SemVerRange
+parseSemVerRange = parse pSemVerRange
+
+-- | Parse a string as an explicit version, or return an error.
+parseSemVer :: Text -> Either ParseError SemVer
+parseSemVer = parse pSemVer
+
+-- | Parses a semantic version.
+pSemVer :: Parser SemVer
+pSemVer = wildcardToSemver <$> pWildCard
+
+pVersionComp :: Parser SemVerRange
+pVersionComp = do
+  comparator <- cmp
+  ver <- pSemVer
+  let func = case comparator of {"=" -> Eq; ">" -> Gt; "<" -> Lt;
+                                 ">=" -> Geq; "<=" -> Leq; "==" -> Eq}
+  return $ func ver
+
+-- | Parses a comparison operator.
+cmp :: Parser String
+cmp = choice $ fmap (try . sstring) [">=", "<=", ">", "<", "==", "="]
+
+-- | Parses versions with an explicit range qualifier (gt, lt, etc).
+pSemVerRangeSingle :: Parser SemVerRange
+pSemVerRangeSingle = choice [
+    wildcardToRange <$> pWildCard,
+    tildeToRange <$> pTildeRange,
+    caratToRange <$> pCaratRange,
+    pVersionComp
+  ]
+
+-- | Parses semantic version ranges joined with Ands and Ors.
+pJoinedSemVerRange :: Parser SemVerRange
+pJoinedSemVerRange = do
+  first <- pSemVerRangeSingle
+  option first $ do
+    lookAhead (sstring "||" <|> cmp) >>= \case
+      "||" -> Or first <$> (sstring "||" *> pJoinedSemVerRange)
+      _ -> And first <$> pJoinedSemVerRange
+
+-- | Parses a hyphenated range.
+pHyphen :: Parser SemVerRange
+pHyphen = hyphenatedRange <$> pWildCard <*> (sstring "-" *> pWildCard)
+
+-- | Parses a "wildcard" (which is a possibly partial semantic version).
+pWildCard :: Parser Wildcard
+pWildCard = try $ do
+  let seps = choice $ map string ["x", "X", "*"]
+  let bound = choice [seps *> pure Nothing, Just <$> pInt']
+  let stripNothings [Nothing] = []
+      stripNothings (Just x:xs) = x : stripNothings xs
+      tag = fmap pack $ many1 $ letter <|> digit <|> char '-'
+  -- Versions can optionally start with the character 'v'
+  optional (char 'v')
+  takeWhile isJust <$> sepBy1 bound (sstring ".") >>= \case
+    [] -> return Any
+    [Just n] -> return $ One n
+    [Just n, Just m] -> return $ Two n m
+    [Just n, Just m, Just o] -> option (Three n m o []) $ do
+      char '-'
+      tags <- tag `sepBy1` char '.'
+      return $ Three n m o tags
+    w -> unexpected ("Invalid version " ++ show w)
+
+-- | Parses a tilde range (~1.2.3).
+pTildeRange :: Parser Wildcard
+pTildeRange = do
+  sstring "~"
+  -- For some reason, including the following operators after
+  -- a tilde is valid, but seems to have no effect.
+  optional $ choice [try $ sstring ">=", sstring ">", sstring "="]
+  pWildCard
+
+-- | Parses a carat range (^1.2.3).
+pCaratRange :: Parser Wildcard
+pCaratRange = sstring "^" *> pWildCard
+
+-- | Top-level parser. Parses a semantic version range.
+pSemVerRange :: Parser SemVerRange
+pSemVerRange = try pHyphen <|> pJoinedSemVerRange
+
+
+-- | Parse a semver from a haskell version. There must be exactly
+-- three numbers in the versionBranch field.
+fromHaskellVersion :: Version -> Either Text SemVer
+fromHaskellVersion v = case versionBranch v of
+  [x, y, z] -> return (x, y, z, []) -- ignoring version tags since deprecated
+  bad -> do
+    let badVer = intercalate "." (map show bad)
+    Left $ pack ("Not a SemVer version: " <> badVer)
