Add stylish-haskell config

This commit is contained in:
Paul Brinkmeier 2025-08-04 10:40:25 +02:00
parent 2a86a80881
commit 865e4e3b87
9 changed files with 542 additions and 55 deletions

481
.stylish-haskell.yaml Normal file
View File

@ -0,0 +1,481 @@
# stylish-haskell configuration file
# ==================================
# The stylish-haskell tool is mainly configured by specifying steps. These steps
# are a list, so they have an order, and one specific step may appear more than
# once (if needed). Each file is processed by these steps in the given order.
steps:
# Convert some ASCII sequences to their Unicode equivalents. This is disabled
# by default.
# - unicode_syntax:
# # In order to make this work, we also need to insert the UnicodeSyntax
# # language pragma. If this flag is set to true, we insert it when it's
# # not already present. You may want to disable it if you configure
# # language extensions using some other method than pragmas. Default:
# # true.
# add_language_pragma: true
# Format module header
#
# Currently, this option is not configurable and will format all exports and
# module declarations to minimize diffs
#
# - module_header:
# # How many spaces use for indentation in the module header.
# indent: 4
#
# # Should export lists be sorted? Sorting is only performed within the
# # export section, as delineated by Haddock comments.
# sort: true
#
# # See `separate_lists` for the `imports` step.
# separate_lists: true
#
# # When to break the "where".
# # Possible values:
# # - exports: only break when there is an explicit export list.
# # - single: only break when the export list counts more than one export.
# # - inline: only break when the export list is too long. This is
# # determined by the `columns` setting. Not applicable when the export
# # list contains comments as newlines will be required.
# # - always: always break before the "where".
# break_where: exports
#
# # Where to put open bracket
# # Possible values:
# # - same_line: put open bracket on the same line as the module name, before the
# # comment of the module
# # - next_line: put open bracket on the next line, after module comment
# open_bracket: next_line
# Format record definitions. This is disabled by default.
#
# You can control the layout of record fields. The only rules that can't be configured
# are these:
#
# - "|" is always aligned with "="
# - "," in fields is always aligned with "{"
# - "}" is likewise always aligned with "{"
#
# - records:
# # How to format equals sign between type constructor and data constructor.
# # Possible values:
# # - "same_line" -- leave "=" AND data constructor on the same line as the type constructor.
# # - "indent N" -- insert a new line and N spaces from the beginning of the next line.
# equals: "indent 2"
#
# # How to format first field of each record constructor.
# # Possible values:
# # - "same_line" -- "{" and first field goes on the same line as the data constructor.
# # - "indent N" -- insert a new line and N spaces from the beginning of the data constructor
# first_field: "indent 2"
#
# # How many spaces to insert between the column with "," and the beginning of the comment in the next line.
# field_comment: 2
#
# # How many spaces to insert before "deriving" clause. Deriving clauses are always on separate lines.
# deriving: 2
#
# # How many spaces to insert before "via" clause counted from indentation of deriving clause
# # Possible values:
# # - "same_line" -- "via" part goes on the same line as "deriving" keyword.
# # - "indent N" -- insert a new line and N spaces from the beginning of "deriving" keyword.
# via: "indent 2"
#
# # Sort typeclass names in the "deriving" list alphabetically.
# sort_deriving: true
#
# # Whether or not to break enums onto several lines
# #
# # Default: false
# break_enums: false
#
# # Whether or not to break single constructor data types before `=` sign
# #
# # Default: true
# break_single_constructors: true
#
# # Whether or not to curry constraints on function.
# #
# # E.g: @allValues :: Enum a => Bounded a => Proxy a -> [a]@
# #
# # Instead of @allValues :: (Enum a, Bounded a) => Proxy a -> [a]@
# #
# # Default: false
# curried_context: false
# Align the right hand side of some elements. This is quite conservative
# and only applies to statements where each element occupies a single
# line.
# Possible values:
# - always - Always align statements.
# - adjacent - Align statements that are on adjacent lines in groups.
# - never - Never align statements.
# All default to always.
- simple_align:
cases: always
top_level_patterns: always
records: always
multi_way_if: always
# Import cleanup
- imports:
# There are different ways we can align names and lists.
#
# - global: Align the import names and import list throughout the entire
# file.
#
# - file: Like global, but don't add padding when there are no qualified
# imports in the file.
#
# - group: Only align the imports per group (a group is formed by adjacent
# import lines).
#
# - none: Do not perform any alignment.
#
# Default: global.
align: none
# The following options affect only import list alignment.
#
# List align has following options:
#
# - after_alias: Import list is aligned with end of import including
# 'as' and 'hiding' keywords.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - with_alias: Import list is aligned with start of alias or hiding.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - with_module_name: Import list is aligned `list_padding` spaces after
# the module name.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# init, last, length)
#
# This is mainly intended for use with `pad_module_names: false`.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# init, last, length, scanl, scanr, take, drop,
# sort, nub)
#
# - new_line: Import list starts always on new line.
#
# > import qualified Data.List as List
# > (concat, foldl, foldr, head, init, last, length)
#
# - repeat: Repeat the module name to align the import list.
#
# > import qualified Data.List as List (concat, foldl, foldr, head)
# > import qualified Data.List as List (init, last, length)
#
# Default: after_alias
list_align: after_alias
# Right-pad the module names to align imports in a group:
#
# - true: a little more readable
#
# > import qualified Data.List as List (concat, foldl, foldr,
# > init, last, length)
# > import qualified Data.List.Extra as List (concat, foldl, foldr,
# > init, last, length)
#
# - false: diff-safe
#
# > import qualified Data.List as List (concat, foldl, foldr, init,
# > last, length)
# > import qualified Data.List.Extra as List (concat, foldl, foldr,
# > init, last, length)
#
# Default: true
pad_module_names: true
# Long list align style takes effect when import is too long. This is
# determined by 'columns' setting.
#
# - inline: This option will put as much specs on same line as possible.
#
# - new_line: Import list will start on new line.
#
# - new_line_multiline: Import list will start on new line when it's
# short enough to fit to single line. Otherwise it'll be multiline.
#
# - multiline: One line per import list entry.
# Type with constructor list acts like single import.
#
# > import qualified Data.Map as M
# > ( empty
# > , singleton
# > , ...
# > , delete
# > )
#
# Default: inline
long_list_align: multiline
# Align empty list (importing instances)
#
# Empty list align has following options
#
# - inherit: inherit list_align setting
#
# - right_after: () is right after the module name:
#
# > import Vector.Instances ()
#
# Default: inherit
empty_list_align: inherit
# List padding determines indentation of import list on lines after import.
# This option affects 'long_list_align'.
#
# - <integer>: constant value
#
# - module_name: align under start of module name.
# Useful for 'file' and 'group' align settings.
#
# Default: 4
list_padding: 4
# Separate lists option affects formatting of import list for type
# or class. The only difference is single space between type and list
# of constructors, selectors and class functions.
#
# - true: There is single space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable (fold, foldl, foldMap))
#
# - false: There is no space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable(fold, foldl, foldMap))
#
# Default: true
separate_lists: true
# Space surround option affects formatting of import lists on a single
# line. The only difference is single space after the initial
# parenthesis and a single space before the terminal parenthesis.
#
# - true: There is single space associated with the enclosing
# parenthesis.
#
# > import Data.Foo ( foo )
#
# - false: There is no space associated with the enclosing parenthesis
#
# > import Data.Foo (foo)
#
# Default: false
space_surround: false
# Post qualify option moves any qualifies found in import declarations
# to the end of the declaration. This also adjust padding for any
# unqualified import declarations.
#
# - true: Qualified as <module name> is moved to the end of the
# declaration.
#
# > import Data.Bar
# > import Data.Foo qualified as F
#
# - false: Qualified remains in the default location and unqualified
# imports are padded to align with qualified imports.
#
# > import Data.Bar
# > import qualified Data.Foo as F
#
# Default: false
post_qualify: false
# Automatically group imports based on their module names, with
# a blank line separating each group. Groups are ordered in
# alphabetical order.
#
# By default, this groups by the first part of each module's
# name (Control.* will be grouped together, Data.*... etc), but
# this can be configured with the group_patterns setting.
#
# When enabled, this rewrites existing blank lines and groups.
#
# - true: Group imports by the first part of the module name.
#
# > import Control.Applicative
# > import Control.Monad
# > import Control.Monad.MonadError
# >
# > import Data.Functor
#
# - false: Keep import groups as-is (still sorting and
# formatting the imports within each group)
#
# > import Control.Monad
# > import Data.Functor
# >
# > import Control.Applicative
# > import Control.Monad.MonadError
#
# Default: false
group_imports: false
# A list of rules specifying how to group modules and how to
# order the groups.
#
# Each rule has a match field; the rule only applies to module
# names matched by this pattern. Patterns are POSIX extended
# regular expressions; see the documentation of Text.Regex.TDFA
# for details:
# https://hackage.haskell.org/package/regex-tdfa-1.3.1.2/docs/Text-Regex-TDFA.html
#
# Rules are processed in order, so only the *first* rule that
# matches a specific module will apply. Any module names that do
# not match a single rule will be put into a single group at the
# end of the import block.
#
# Example: group MyApp modules first, with everything else in
# one group at the end.
#
# group_rules:
# - match: "^MyApp\\>"
#
# > import MyApp
# > import MyApp.Foo
# >
# > import Control.Monad
# > import MyApps
# > import Test.MyApp
#
# A rule can also optionally have a sub_group pattern. Imports
# that match the rule will be broken up into further groups by
# the part of the module name matched by the sub_group pattern.
#
# Example: group MyApp modules first, then everything else
# sub-grouped by the first part of the module name.
#
# group_rules:
# - match: "^MyApp\\>"
# - match: "."
# sub_group: "^[^.]+"
#
# > import MyApp
# > import MyApp.Foo
# >
# > import Control.Applicative
# > import Control.Monad
# >
# > import Data.Map
#
# A pattern only needs to match part of the module name, which
# could be in the middle. You can use ^pattern to anchor to the
# beginning of the module name, pattern$ to anchor to the end
# and ^pattern$ to force a full match. Example:
#
# - "Test\\." would match "Test.Foo" and "Foo.Test.Lib"
# - "^Test\\." would match "Test.Foo" but not "Foo.Test.Lib"
# - "\\.Test$" would match "Foo.Test" but not "Foo.Test.Lib"
# - "^Test$" would *only* match "Test"
#
# You can use \\< and \\> to anchor against the beginning and
# end of words, respectively. For example:
#
# - "^Test\\." would match "Test.Foo" but not "Test" or "Tests"
# - "^Test\\>" would match "Test.Foo" and "Test", but not
# "Tests"
#
# The default is a single rule that matches everything and
# sub-groups based on the first component of the module name.
#
# Default: [{ "match" : ".*", "sub_group": "^[^.]+" }]
group_rules:
- match: ".*"
sub_group: "^[^.]+"
# Language pragmas
- language_pragmas:
# We can generate different styles of language pragma lists.
#
# - vertical: Vertical-spaced language pragmas, one per line.
#
# - compact: A more compact style.
#
# - compact_line: Similar to compact, but wrap each line with
# `{-# LANGUAGE #-}'.
#
# - vertical_compact: Similar to vertical, but use only one language pragma.
#
# Default: vertical.
style: compact_line
# Align affects alignment of closing pragma brackets.
#
# - true: Brackets are aligned in same column.
#
# - false: Brackets are not aligned together. There is only one space
# between actual import and closing bracket.
#
# Default: true
align: true
# stylish-haskell can detect redundancy of some language pragmas. If this
# is set to true, it will remove those redundant pragmas. Default: true.
remove_redundant: true
# Language prefix to be used for pragma declaration, this allows you to
# use other options non case-sensitive like "language" or "Language".
# If a non correct String is provided, it will default to: LANGUAGE.
language_prefix: LANGUAGE
# Replace tabs by spaces. This is disabled by default.
# - tabs:
# # Number of spaces to use for each tab. Default: 8, as specified by the
# # Haskell report.
# spaces: 8
# Remove trailing whitespace
- trailing_whitespace: {}
# Squash multiple spaces between the left and right hand sides of some
# elements into single spaces. Basically, this undoes the effect of
# simple_align but is a bit less conservative.
# - squash: {}
# A common setting is the number of columns (parts of) code will be wrapped
# to. Different steps take this into account.
#
# Set this to null to disable all line wrapping.
#
# Default: 80.
columns: 100
# By default, line endings are converted according to the OS. You can override
# preferred format here.
#
# - native: Native newline format. CRLF on Windows, LF on other OSes.
#
# - lf: Convert to LF ("\n").
#
# - crlf: Convert to CRLF ("\r\n").
#
# Default: native.
newline: native
# Sometimes, language extensions are specified in a cabal file or from the
# command line instead of using language pragmas in the file. stylish-haskell
# needs to be aware of these, so it can parse the file correctly.
#
# No language extensions are enabled by default.
# language_extensions:
# - TemplateHaskell
# - QuasiQuotes
# Attempt to find the cabal file in ancestors of the current directory, and
# parse options (currently only language extensions) from that.
#
# Default: true
cabal: true

View File

@ -1,6 +1,6 @@
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Main (main) where module Main (main) where
@ -11,18 +11,29 @@ import Data.Proxy (Proxy (..))
import Data.Text (Text) import Data.Text (Text)
import Network.HTTP.Media ((//), (/:)) import Network.HTTP.Media ((//), (/:))
import Network.HTTP.Types (status200) import Network.HTTP.Types (status200)
import Servant (Accept (..), Get, Server, serve, MimeRender (..), (:<|>) (..), (:>), Raw, Capture, Tagged (..)) import Servant
( Accept (..)
, Capture
, Get
, MimeRender (..)
, Raw
, Server
, Tagged (..)
, serve
, (:<|>) (..)
, (:>)
)
import System.FilePath ((</>)) import System.FilePath ((</>))
import Text.Blaze.Html5 ((!))
import Text.Blaze.Html.Renderer.Utf8 (renderHtml) import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
import Text.Blaze.Html5 ((!))
import Text.Printf (printf) import Text.Printf (printf)
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Database.PostgreSQL.Opium as Opium import qualified Database.PostgreSQL.Opium as Opium
import Network.Wai (responseFile)
import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Handler.Warp as Warp
import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A import qualified Text.Blaze.Html5.Attributes as A
import Network.Wai (responseFile)
import Yore.DB (DayFile (..)) import Yore.DB (DayFile (..))
import Yore.Time (addYears, getCurrentDay) import Yore.Time (addYears, getCurrentDay)
@ -67,7 +78,7 @@ server connPool = rootR :<|> todayR :<|> apiTodayR
res <- withResource connPool $ DB.getDayFileByIssue dateThen issue res <- withResource connPool $ DB.getDayFileByIssue dateThen issue
dayFile <- either throwIO pure res dayFile <- either throwIO pure res
let fullPath = "download" </> dayFile.relative_path let fullPath = "download" </> dayFile.relative_path
respond $ responseFile status200 [("content-type", "application/pdf")] fullPath Nothing respond $ responseFile status200 [("content-type", "application/pdf")] fullPath Nothing
newtype RootModel = RootModel Text newtype RootModel = RootModel Text

View File

@ -74,17 +74,12 @@
pkgs.cabal-install pkgs.cabal-install
pkgs.haskellPackages.implicit-hie pkgs.haskellPackages.implicit-hie
pkgs.haskell-language-server pkgs.haskell-language-server
pkgs.stylish-haskell
pkgs.postgresql pkgs.postgresql
pkgs.dbmate pkgs.dbmate
]; ];
overrides = addOpium; overrides = addOpium;
}).env; }).env;
just-hls = pkgs.mkShell {
packages = [
pkgs.haskell-language-server
pkgs.gcc
];
};
}; };
}); });
} }

View File

@ -1,6 +1,6 @@
{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
module Yore.DB module Yore.DB
( DayIndex (..) ( DayIndex (..)
@ -23,21 +23,21 @@ import qualified Database.PostgreSQL.Opium as Opium
data Table = Table data Table = Table
{ schema :: String { schema :: String
, name :: String , name :: String
} deriving (Show, Generic, Opium.FromRow) } deriving (Show, Generic, Opium.FromRow)
getTables :: Opium.Connection -> IO (Either Opium.Error [Table]) getTables :: Opium.Connection -> IO (Either Opium.Error [Table])
getTables = Opium.fetch_ "SELECT table_schema AS schema, table_name AS name FROM information_schema.tables" getTables = Opium.fetch_ "SELECT table_schema AS schema, table_name AS name FROM information_schema.tables"
data DayIndex = DayIndex data DayIndex = DayIndex
{ day_index_id :: Int { day_index_id :: Int
, day :: Day , day :: Day
} deriving (Show, Generic, Opium.FromRow) } deriving (Show, Generic, Opium.FromRow)
data DayFile = DayFile data DayFile = DayFile
{ day_file_id :: Int { day_file_id :: Int
, day_index_id :: Int , day_index_id :: Int
, label :: Text , label :: Text
, relative_path :: FilePath , relative_path :: FilePath
} deriving (Show, Generic, Opium.FromRow) } deriving (Show, Generic, Opium.FromRow)

View File

@ -7,7 +7,7 @@ import Data.Maybe (fromJust)
import Data.Text (Text) import Data.Text (Text)
import Network.HTTP.Req hiding (queryParam) import Network.HTTP.Req hiding (queryParam)
import System.Directory (createDirectoryIfMissing) import System.Directory (createDirectoryIfMissing)
import System.FilePath (makeRelative, takeExtension, (</>), (<.>)) import System.FilePath (makeRelative, takeExtension, (<.>), (</>))
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as Text import qualified Data.Text as Text

View File

@ -1,5 +1,5 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
module Yore.Index (createEntries) where module Yore.Index (createEntries) where

View File

@ -1,6 +1,6 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Yore.Repl (connect, exec, getToday, indexDay, addYears) where module Yore.Repl (connect, exec, getToday, indexDay, addYears) where

View File

@ -1,38 +1,38 @@
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
module Yore.Scrape (Issue (..), getIssuesByDay) where module Yore.Scrape (Issue (..), getIssuesByDay) where
import Control.Lens (each, (^?), (^..)) import Control.Lens (each, (^..), (^?))
import Control.Monad (guard) import Control.Monad (guard)
import Data.Char (chr, isDigit, ord) import Data.Char (chr, isDigit, ord)
import Data.Maybe (listToMaybe, mapMaybe) import Data.Maybe (listToMaybe, mapMaybe)
import Data.Text (Text) import Data.Text (Text)
import Data.Time (Year, MonthOfYear, Day, fromGregorian, toGregorian) import Data.Time (Day, MonthOfYear, Year, fromGregorian, toGregorian)
import Network.HTTP.Req import Network.HTTP.Req
( GET (..) ( BsResponse
, BsResponse , GET (..)
, NoReqBody (..) , NoReqBody (..)
, Req , Req
, bsResponse , bsResponse
, defaultHttpConfig , defaultHttpConfig
, formToQuery , formToQuery
, https , https
, req , req
, responseBody , responseBody
, responseStatusCode , responseStatusCode
, runReq , runReq
, (/:) , (/:)
) )
import Text.HTML.Parser (Token (..), Attr (..), parseTokens) import Text.HTML.Parser (Attr (..), Token (..), parseTokens)
import Text.Printf (printf) import Text.Printf (printf)
import Text.URI (URI) import Text.URI (URI)
import Text.URI.Lens (queryParam, uriPath, uriQuery, unRText) import Text.URI.Lens (queryParam, unRText, uriPath, uriQuery)
import Text.URI.QQ (queryKey) import Text.URI.QQ (queryKey)
import qualified Data.Text as Text import qualified Data.Text as Text
@ -41,7 +41,7 @@ import qualified Text.URI as URI
-- | A single issue of a newspaper. There may be multiple isses per day. -- | A single issue of a newspaper. There may be multiple isses per day.
data Issue = Issue data Issue = Issue
{ day :: Day { day :: Day
, label :: Text , label :: Text
-- ^ A human-readable label for the issue, e.g. @1. Blatt@, @Sportblatt@ or @Handelsblatt@. -- ^ A human-readable label for the issue, e.g. @1. Blatt@, @Sportblatt@ or @Handelsblatt@.
, url :: Text , url :: Text
@ -147,7 +147,7 @@ getShowday t = do
getAHref :: Token -> Maybe Text getAHref :: Token -> Maybe Text
getAHref (TagOpen "a" attrs) = getAttr "href" attrs getAHref (TagOpen "a" attrs) = getAttr "href" attrs
getAHref _ = Nothing getAHref _ = Nothing
getRefreshTarget :: Token -> Maybe Text getRefreshTarget :: Token -> Maybe Text
getRefreshTarget (TagOpen "meta" attrs) = do getRefreshTarget (TagOpen "meta" attrs) = do
@ -180,14 +180,14 @@ getParam param uri =
decodeDecimalEntities :: Text -> Text decodeDecimalEntities :: Text -> Text
decodeDecimalEntities = Text.pack . q0 . Text.unpack decodeDecimalEntities = Text.pack . q0 . Text.unpack
where where
q0 "" = "" q0 "" = ""
q0 ('&' : '#' : r) = q1 0 r q0 ('&' : '#' : r) = q1 0 r
q0 (c : r) = c : q0 r q0 (c : r) = c : q0 r
-- Silently swallows broken entities at the end of the string... -- Silently swallows broken entities at the end of the string...
q1 _ "" = "" q1 _ "" = ""
q1 acc (';' : r) = chr acc : q0 r q1 acc (';' : r) = chr acc : q0 r
q1 acc (c : r) q1 acc (c : r)
| isDigit c = q1 (10 * acc + ord c - ord '0') r | isDigit c = q1 (10 * acc + ord c - ord '0') r
-- Or if they end unexpectedly. -- Or if they end unexpectedly.
| otherwise = c : q0 r | otherwise = c : q0 r

View File

@ -1,7 +1,7 @@
module Yore.Time (addYears, getCurrentDay) where module Yore.Time (addYears, getCurrentDay) where
import Data.Time (Day, addGregorianYearsClip, isLeapYear, toGregorian) import Data.Time (Day, addGregorianYearsClip, isLeapYear, toGregorian)
import Data.Time.LocalTime (localDay, zonedTimeToLocalTime, getZonedTime) import Data.Time.LocalTime (getZonedTime, localDay, zonedTimeToLocalTime)
addYears :: Integer -> Day -> Maybe Day addYears :: Integer -> Day -> Maybe Day
addYears yearOffset date addYears yearOffset date
@ -9,7 +9,7 @@ addYears yearOffset date
Nothing Nothing
| otherwise = | otherwise =
Just $ addGregorianYearsClip yearOffset date Just $ addGregorianYearsClip yearOffset date
where where
(year, month, day) = toGregorian date (year, month, day) = toGregorian date
year' = year + yearOffset year' = year + yearOffset