feat: implemented the program
This commit is contained in:
commit
cd5de79e24
22 changed files with 930 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.stack-work/
|
||||||
|
*~
|
11
CHANGELOG.md
Normal file
11
CHANGELOG.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Changelog for `solvedoku`
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to the
|
||||||
|
[Haskell Package Versioning Policy](https://pvp.haskell.org/).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.1.0.0 - YYYY-MM-DD
|
1
README.md
Normal file
1
README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# solvedoku
|
2
Setup.hs
Normal file
2
Setup.hs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Distribution.Simple
|
||||||
|
main = defaultMain
|
36
app/Main.hs
Normal file
36
app/Main.hs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
module Main (main) where
|
||||||
|
import qualified Data.Text.IO as Text
|
||||||
|
import Text.Megaparsec (errorBundlePretty, runParser)
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import qualified Sudoku
|
||||||
|
import qualified Sudoku.Solve as Solve
|
||||||
|
import qualified Sudoku.Render as Render
|
||||||
|
import Sudoku.State (State)
|
||||||
|
import Options.Applicative (subparser, command, info, Parser, progDesc, execParser)
|
||||||
|
import System.Exit (exitFailure)
|
||||||
|
|
||||||
|
data Mode
|
||||||
|
= Solve
|
||||||
|
| Hints
|
||||||
|
|
||||||
|
parseMode :: Parser Mode
|
||||||
|
parseMode = subparser
|
||||||
|
( command "solve" (info (pure Solve) (progDesc "Solve the supplied sudoku entirely."))
|
||||||
|
<> command "hints" (info (pure Hints) (progDesc "Show hints for the sudoku."))
|
||||||
|
)
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
mode <- execParser (info parseMode mempty)
|
||||||
|
input <- Text.getContents
|
||||||
|
sudoku <- case runParser Sudoku.parse "<stdin>" input of
|
||||||
|
Left errorBundle -> (Text.putStr . Text.pack . errorBundlePretty $ errorBundle) >> exitFailure
|
||||||
|
Right s -> pure s
|
||||||
|
|
||||||
|
Text.putStr $ case mode of
|
||||||
|
Solve -> Text.pack . show $ Solve.solve State sudoku
|
||||||
|
Hints -> let
|
||||||
|
hints = Solve.hints State sudoku
|
||||||
|
|
||||||
|
in Render.hints hints
|
||||||
|
|
66
package.yaml
Normal file
66
package.yaml
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
name: solvedoku
|
||||||
|
version: 0.1.0.0
|
||||||
|
license: AGPL-3.0
|
||||||
|
author: "VegOwOtenks"
|
||||||
|
maintainer: "vegowotenks@jossco.de"
|
||||||
|
copyright: "2025 VegOwOtenks"
|
||||||
|
|
||||||
|
extra-source-files:
|
||||||
|
- README.md
|
||||||
|
- CHANGELOG.md
|
||||||
|
|
||||||
|
# Metadata used when publishing your package
|
||||||
|
# synopsis: Short description of your package
|
||||||
|
# category: Web
|
||||||
|
|
||||||
|
# To avoid duplicated efforts in documentation and dealing with the
|
||||||
|
# complications of embedding Haddock markup inside cabal files, it is
|
||||||
|
# common to point users to the README.md file.
|
||||||
|
description: Please see the README on GitHub at <https://github.com/githubuser/solvedoku#readme>
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- adjunctions
|
||||||
|
- array
|
||||||
|
- base >= 4.7 && < 5
|
||||||
|
- containers
|
||||||
|
- distributive
|
||||||
|
- megaparsec
|
||||||
|
- optparse-applicative
|
||||||
|
- text
|
||||||
|
- QuickCheck
|
||||||
|
|
||||||
|
ghc-options:
|
||||||
|
- -Wall
|
||||||
|
- -Wcompat
|
||||||
|
- -Widentities
|
||||||
|
- -Wincomplete-record-updates
|
||||||
|
- -Wincomplete-uni-patterns
|
||||||
|
- -Wmissing-export-lists
|
||||||
|
- -Wmissing-home-modules
|
||||||
|
- -Wpartial-fields
|
||||||
|
- -Wredundant-constraints
|
||||||
|
|
||||||
|
library:
|
||||||
|
source-dirs: src
|
||||||
|
|
||||||
|
executables:
|
||||||
|
solvedoku-exe:
|
||||||
|
main: Main.hs
|
||||||
|
source-dirs: app
|
||||||
|
ghc-options:
|
||||||
|
- -threaded
|
||||||
|
- -rtsopts
|
||||||
|
- -with-rtsopts=-N
|
||||||
|
dependencies:
|
||||||
|
- solvedoku
|
||||||
|
|
||||||
|
tests:
|
||||||
|
solvedoku-test:
|
||||||
|
main: Spec.hs
|
||||||
|
source-dirs: test
|
||||||
|
ghc-options:
|
||||||
|
- -threaded
|
||||||
|
- -rtsopts
|
||||||
|
- -with-rtsopts=-N
|
||||||
|
dependencies:
|
||||||
|
- solvedoku
|
101
solvedoku.cabal
Normal file
101
solvedoku.cabal
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
cabal-version: 2.2
|
||||||
|
|
||||||
|
-- This file has been generated from package.yaml by hpack version 0.38.1.
|
||||||
|
--
|
||||||
|
-- see: https://github.com/sol/hpack
|
||||||
|
|
||||||
|
name: solvedoku
|
||||||
|
version: 0.1.0.0
|
||||||
|
description: Please see the README on GitHub at <https://github.com/githubuser/solvedoku#readme>
|
||||||
|
homepage: https://github.com/githubuser/solvedoku#readme
|
||||||
|
bug-reports: https://github.com/githubuser/solvedoku/issues
|
||||||
|
author: Author name here
|
||||||
|
maintainer: example@example.com
|
||||||
|
copyright: 2025 Author name here
|
||||||
|
license: BSD-3-Clause
|
||||||
|
license-file: LICENSE
|
||||||
|
build-type: Simple
|
||||||
|
extra-source-files:
|
||||||
|
README.md
|
||||||
|
CHANGELOG.md
|
||||||
|
|
||||||
|
source-repository head
|
||||||
|
type: git
|
||||||
|
location: https://github.com/githubuser/solvedoku
|
||||||
|
|
||||||
|
library
|
||||||
|
exposed-modules:
|
||||||
|
Data.Enum.Util
|
||||||
|
Data.Functor.Representable
|
||||||
|
Data.ImplicitMultiMap
|
||||||
|
Graphics.Text.Box
|
||||||
|
Sudoku
|
||||||
|
Sudoku.Area
|
||||||
|
Sudoku.Field
|
||||||
|
Sudoku.Render
|
||||||
|
Sudoku.Solve
|
||||||
|
Sudoku.State
|
||||||
|
Sudoku.Triple
|
||||||
|
other-modules:
|
||||||
|
Paths_solvedoku
|
||||||
|
autogen-modules:
|
||||||
|
Paths_solvedoku
|
||||||
|
hs-source-dirs:
|
||||||
|
src
|
||||||
|
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints
|
||||||
|
build-depends:
|
||||||
|
QuickCheck
|
||||||
|
, adjunctions
|
||||||
|
, array
|
||||||
|
, base >=4.7 && <5
|
||||||
|
, containers
|
||||||
|
, distributive
|
||||||
|
, megaparsec
|
||||||
|
, optparse-applicative
|
||||||
|
, text
|
||||||
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
executable solvedoku-exe
|
||||||
|
main-is: Main.hs
|
||||||
|
other-modules:
|
||||||
|
Paths_solvedoku
|
||||||
|
autogen-modules:
|
||||||
|
Paths_solvedoku
|
||||||
|
hs-source-dirs:
|
||||||
|
app
|
||||||
|
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
|
||||||
|
build-depends:
|
||||||
|
QuickCheck
|
||||||
|
, adjunctions
|
||||||
|
, array
|
||||||
|
, base >=4.7 && <5
|
||||||
|
, containers
|
||||||
|
, distributive
|
||||||
|
, megaparsec
|
||||||
|
, optparse-applicative
|
||||||
|
, solvedoku
|
||||||
|
, text
|
||||||
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
test-suite solvedoku-test
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
main-is: Spec.hs
|
||||||
|
other-modules:
|
||||||
|
Paths_solvedoku
|
||||||
|
autogen-modules:
|
||||||
|
Paths_solvedoku
|
||||||
|
hs-source-dirs:
|
||||||
|
test
|
||||||
|
ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
|
||||||
|
build-depends:
|
||||||
|
QuickCheck
|
||||||
|
, adjunctions
|
||||||
|
, array
|
||||||
|
, base >=4.7 && <5
|
||||||
|
, containers
|
||||||
|
, distributive
|
||||||
|
, megaparsec
|
||||||
|
, optparse-applicative
|
||||||
|
, solvedoku
|
||||||
|
, text
|
||||||
|
default-language: Haskell2010
|
4
src/Data/Enum/Util.hs
Normal file
4
src/Data/Enum/Util.hs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module Data.Enum.Util (enumerate) where
|
||||||
|
|
||||||
|
enumerate :: (Bounded a, Enum a) => [a]
|
||||||
|
enumerate = [minBound..maxBound]
|
21
src/Data/Functor/Representable.hs
Normal file
21
src/Data/Functor/Representable.hs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
module Data.Functor.Representable (set, get, adjust) where
|
||||||
|
import Data.Functor.Rep (Representable(index, Rep, tabulate))
|
||||||
|
|
||||||
|
get :: Representable f => f a -> Rep f -> a
|
||||||
|
get = index
|
||||||
|
|
||||||
|
set :: (Representable f, Eq (Rep f)) => Rep f -> a -> f a -> f a
|
||||||
|
set i x old = tabulate override
|
||||||
|
where
|
||||||
|
override j
|
||||||
|
| i == j = x
|
||||||
|
| otherwise = index old j
|
||||||
|
|
||||||
|
adjust :: (Representable f, Eq (Rep f)) => Rep f -> (a -> a) -> f a -> f a
|
||||||
|
adjust i f old = tabulate override
|
||||||
|
where
|
||||||
|
override j
|
||||||
|
| i == j = f (index old j)
|
||||||
|
| otherwise = index old j
|
||||||
|
|
15
src/Data/ImplicitMultiMap.hs
Normal file
15
src/Data/ImplicitMultiMap.hs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
module Data.ImplicitMultiMap (ImplicitMultiMap, DeriveKey(..), empty) where
|
||||||
|
import Data.Kind (Type)
|
||||||
|
import Data.Map (Map)
|
||||||
|
import Data.Set (Set)
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
|
class DeriveKey a where
|
||||||
|
type KeyOf a :: Type
|
||||||
|
deriveKey :: a -> KeyOf a
|
||||||
|
|
||||||
|
newtype ImplicitMultiMap a = ImplicitMultiMap (Map (KeyOf a) (Set a))
|
||||||
|
|
||||||
|
empty :: ImplicitMultiMap a
|
||||||
|
empty = ImplicitMultiMap Map.empty
|
126
src/Graphics/Text/Box.hs
Normal file
126
src/Graphics/Text/Box.hs
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE OverloadedRecordDot #-}
|
||||||
|
{-# LANGUAGE NPlusKPatterns #-}
|
||||||
|
{-# LANGUAGE MultiWayIf #-}
|
||||||
|
module Graphics.Text.Box (Box, square, Border(..), asciiBorder, grid, asciiGrid, size, lookup, render) where
|
||||||
|
|
||||||
|
import Prelude hiding (lookup)
|
||||||
|
|
||||||
|
import Data.Array.IArray (Array, genArray)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Array.IArray as Array
|
||||||
|
import Control.Monad (join)
|
||||||
|
import Data.Bifunctor ( Bifunctor(bimap) )
|
||||||
|
import qualified Data.Maybe as Maybe
|
||||||
|
import qualified Data.Foldable as Foldable
|
||||||
|
import Control.Arrow (Arrow ((***)))
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
|
||||||
|
data Box
|
||||||
|
= Raw (Array (Int, Int) Char)
|
||||||
|
| Grid (Array (Int, Int) Box) (Maybe Border)
|
||||||
|
|
||||||
|
data Border = Border
|
||||||
|
{ top, right, left, bottom, topRight, bottomRight, bottomLeft, topLeft, crosssection, verticalBetween, horizontalBetween :: Char }
|
||||||
|
|
||||||
|
asciiBorder :: Border
|
||||||
|
asciiBorder = Border {top='-', right='|', left='|', bottom='-', topRight='\\', bottomRight='/', bottomLeft='\\', topLeft='/', crosssection='+', verticalBetween='|', horizontalBetween='-'}
|
||||||
|
|
||||||
|
type Row = Int
|
||||||
|
type Column = Int
|
||||||
|
|
||||||
|
square :: Int -> ((Row, Column) -> Char) -> Box
|
||||||
|
square dim f = Raw $ genArray ((0, 0), (dim - 1, dim - 1)) f
|
||||||
|
|
||||||
|
grid :: Maybe Border -> (Int, Int) -> ((Int, Int) -> Box) -> Box
|
||||||
|
grid border (rows, cols) f = let
|
||||||
|
|
||||||
|
boxes = genArray ((0, 0), (rows - 1, cols - 1)) f
|
||||||
|
|
||||||
|
in Grid boxes border
|
||||||
|
|
||||||
|
asciiGrid :: (Int, Int) -> ((Int, Int) -> Box) -> Box
|
||||||
|
asciiGrid = grid $ Just asciiBorder
|
||||||
|
|
||||||
|
gridCellSize :: Array (Int, Int) Box -> (Int, Int)
|
||||||
|
gridCellSize = let
|
||||||
|
updateMaxSize box (height, width) = (max height *** max width) $ size box
|
||||||
|
in Foldable.foldr updateMaxSize (0, 0)
|
||||||
|
|
||||||
|
size :: Box -> (Int, Int)
|
||||||
|
size = \case
|
||||||
|
Raw chars -> arraySize chars -- snd takes the higher bound, +1 because the array has inclusive ranges
|
||||||
|
Grid boxes border -> let
|
||||||
|
|
||||||
|
(boxRows, boxColumns) = arraySize boxes
|
||||||
|
(cellHeight, cellWidth) = gridCellSize boxes
|
||||||
|
borderSize = case border of
|
||||||
|
Just _ -> 1
|
||||||
|
Nothing -> 0
|
||||||
|
|
||||||
|
in (boxRows * (cellHeight + borderSize) + 1 , boxColumns * (cellWidth + borderSize) + 1)
|
||||||
|
|
||||||
|
arraySize :: Array (Int, Int) a -> (Int, Int)
|
||||||
|
arraySize = join bimap (+1) . snd . Array.bounds
|
||||||
|
|
||||||
|
orElse :: Maybe a -> a -> a
|
||||||
|
orElse option x = Maybe.fromMaybe x option
|
||||||
|
|
||||||
|
lookup :: Box -> (Int, Int) -> Maybe Char
|
||||||
|
lookup b index@(row, col) = case b of
|
||||||
|
Raw chars -> chars Array.!? index
|
||||||
|
Grid boxes borderM -> let
|
||||||
|
|
||||||
|
(boxRows, boxColumns) = arraySize boxes
|
||||||
|
(boxHeight, boxWidth) = gridCellSize boxes
|
||||||
|
|
||||||
|
in case borderM of
|
||||||
|
|
||||||
|
Nothing -> let
|
||||||
|
(boxRow, boxRowIndex) = row `divMod` boxHeight
|
||||||
|
(boxCol, boxColIndex) = col `divMod` boxWidth
|
||||||
|
in do
|
||||||
|
innerBox <- boxes Array.!? (boxRow, boxCol)
|
||||||
|
lookup innerBox (boxRowIndex, boxColIndex)
|
||||||
|
|
||||||
|
Just border -> if
|
||||||
|
| row == 0 && col == 0 -> Just border.topLeft
|
||||||
|
| row == 0 -> Just border.top
|
||||||
|
| col == 0 -> Just border.left
|
||||||
|
-- ^ outer border cases are easy
|
||||||
|
| otherwise -> let
|
||||||
|
row' = row - 1
|
||||||
|
col' = col - 1
|
||||||
|
(boxRow, boxRowIndex) = row' `divMod` (boxHeight + 1)
|
||||||
|
(boxCol, boxColIndex) = col' `divMod` (boxWidth + 1)
|
||||||
|
|
||||||
|
in if
|
||||||
|
| boxRowIndex == boxHeight && boxColIndex == boxWidth -> Just $
|
||||||
|
if boxRow == boxRows - 1 && boxCol == boxColumns - 1 -- is it the last row and col?
|
||||||
|
then border.bottomRight
|
||||||
|
else border.crosssection
|
||||||
|
| boxRowIndex == boxHeight -> Just $
|
||||||
|
if boxRow == boxRows - 1 -- is it the last row?
|
||||||
|
then border.bottom
|
||||||
|
else border.horizontalBetween
|
||||||
|
| boxColIndex == boxWidth -> Just $
|
||||||
|
if boxCol == boxColumns - 1 -- is it the last column?
|
||||||
|
then border.right
|
||||||
|
else border.verticalBetween
|
||||||
|
| otherwise -> do -- it's not at the border between boxes at all
|
||||||
|
innerBox <- boxes Array.!? (boxRow, boxCol)
|
||||||
|
lookup innerBox (boxRowIndex, boxColIndex)
|
||||||
|
|
||||||
|
render :: Box -> Text
|
||||||
|
render box = let
|
||||||
|
(rows, cols) = join bimap pred $ size box
|
||||||
|
in Text.unlines
|
||||||
|
[
|
||||||
|
Text.pack [
|
||||||
|
lookup box (row, col) `orElse` ' '
|
||||||
|
| col <- [0..cols]
|
||||||
|
]
|
||||||
|
| row <- [0..rows]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
125
src/Sudoku.hs
Normal file
125
src/Sudoku.hs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
-- | A t'Sudoku' is a 3x3 grid of 3x3 grids.
|
||||||
|
|
||||||
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
|
{-# LANGUAGE UndecidableInstances #-}
|
||||||
|
{-# LANGUAGE DerivingVia #-}
|
||||||
|
{-# LANGUAGE InstanceSigs #-}
|
||||||
|
{-# LANGUAGE DeriveTraversable #-}
|
||||||
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
module Sudoku (Sudoku(..), FieldIndex, parse, entireRowOf, entireColumnOf, entireAreaOf, translateIntIndex, orderedIntIndices, orderedIndices, affectedFieldsOf) where
|
||||||
|
|
||||||
|
import Sudoku.Area (Area)
|
||||||
|
import Data.Functor.Compose (Compose)
|
||||||
|
import Text.Megaparsec (Parsec)
|
||||||
|
import Data.Void (Void)
|
||||||
|
import Sudoku.Field (Field)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.Array.IArray (Array)
|
||||||
|
import Data.Array (listArray)
|
||||||
|
import qualified Data.List as List
|
||||||
|
import qualified Sudoku.Field as Field
|
||||||
|
import qualified Text.Megaparsec.Char as Megaparsec
|
||||||
|
import Control.Monad (replicateM, join)
|
||||||
|
import Control.Monad.Representable.Reader
|
||||||
|
import Data.Distributive (Distributive (..))
|
||||||
|
import qualified Data.Array.IArray as Array
|
||||||
|
import Sudoku.Triple (Index)
|
||||||
|
import Data.Enum.Util (enumerate)
|
||||||
|
import Data.Bifunctor (bimap)
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
import Data.Set (Set)
|
||||||
|
import qualified Data.Functor.Rep as Representable
|
||||||
|
|
||||||
|
-- | Composing two areas produces a Sudoku.
|
||||||
|
--
|
||||||
|
-- Index order is:
|
||||||
|
-- - outer row
|
||||||
|
-- - outer column
|
||||||
|
-- - inner row
|
||||||
|
-- - inner column
|
||||||
|
|
||||||
|
type FieldIndex = ((Index, Index), (Index, Index))
|
||||||
|
|
||||||
|
newtype Sudoku a = Sudoku { get :: Compose Area Area a }
|
||||||
|
deriving stock (Foldable, Traversable)
|
||||||
|
deriving newtype (Functor, Representable, Show)
|
||||||
|
|
||||||
|
instance Distributive Sudoku where
|
||||||
|
distribute :: Functor f => f (Sudoku a) -> Sudoku (f a)
|
||||||
|
distribute = Sudoku . distribute . fmap get
|
||||||
|
|
||||||
|
parseSudokuArray :: Parsec Void Text (Array (Int, Int) (Maybe Field))
|
||||||
|
parseSudokuArray = listArray ((0, 0), (8, 8)) . List.concat <$> (9 `times` sudokuLine)
|
||||||
|
|
||||||
|
sudokuLine :: Parsec Void Text [Maybe Field]
|
||||||
|
sudokuLine = (9 `times` Field.parse) <* Megaparsec.eol
|
||||||
|
|
||||||
|
times :: Monad m => Int -> m a -> m [a]
|
||||||
|
times = replicateM
|
||||||
|
|
||||||
|
-- | Numbers are read from digits, 0 is for empty fields.
|
||||||
|
|
||||||
|
parse :: Parsec Void Text (Sudoku (Maybe Field))
|
||||||
|
parse = do
|
||||||
|
numbers <- parseSudokuArray
|
||||||
|
|
||||||
|
let lookupIndex ix = numbers Array.! translateIndex ix
|
||||||
|
|
||||||
|
pure (tabulate lookupIndex)
|
||||||
|
|
||||||
|
translateIndex :: FieldIndex -> (Int, Int)
|
||||||
|
translateIndex ((outerRow, outerCol), (innerRow, innerCol)) = let
|
||||||
|
translatePair major minor = fromEnum major * 3 + fromEnum minor
|
||||||
|
row = translatePair outerRow innerRow
|
||||||
|
col = translatePair outerCol innerCol
|
||||||
|
in (row, col)
|
||||||
|
|
||||||
|
translateIntIndex :: (Int, Int) -> FieldIndex
|
||||||
|
translateIntIndex (row, col) = let
|
||||||
|
|
||||||
|
(outerRow, innerRow) = join bimap toEnum $ row `divMod` 3
|
||||||
|
(outerCol, innerCol) = join bimap toEnum $ col `divMod` 3
|
||||||
|
|
||||||
|
in ((outerRow, outerCol), (innerRow, innerCol))
|
||||||
|
|
||||||
|
orderedIntIndices :: (Enum a, Num a) => [[(a, a)]]
|
||||||
|
orderedIntIndices = [ [(i, j) | j <- [0..8]] | i <- [0..8]]
|
||||||
|
|
||||||
|
orderedIndices :: [[FieldIndex]]
|
||||||
|
orderedIndices = (translateIntIndex <$>) <$> orderedIntIndices
|
||||||
|
|
||||||
|
-- >>> import Data.Functor.Rep
|
||||||
|
-- >>> :kind! Rep Sudoku
|
||||||
|
-- Rep Sudoku :: *
|
||||||
|
-- = ((Index, Index), (Index, Index))
|
||||||
|
|
||||||
|
-- | Calculate the indices of all the fields in the same row as the argument.
|
||||||
|
|
||||||
|
entireRowOf :: FieldIndex -> [FieldIndex]
|
||||||
|
entireRowOf ((_, outerColumn), (_, innerColumn)) = [((outerRow, outerColumn), (innerRow, innerColumn)) | outerRow <- enumerate, innerRow <- enumerate]
|
||||||
|
|
||||||
|
-- | Calculate the indices of all the fields in the same column as the argument.
|
||||||
|
|
||||||
|
entireColumnOf :: FieldIndex -> [FieldIndex]
|
||||||
|
entireColumnOf ((outerRow, _), (innerRow, _)) = do
|
||||||
|
outerColumn <- enumerate
|
||||||
|
innerColumn <- enumerate
|
||||||
|
pure ((outerRow, outerColumn), (innerRow, innerColumn))
|
||||||
|
|
||||||
|
-- | Calculate all the indices of the fields in the same area as the argument.
|
||||||
|
|
||||||
|
entireAreaOf :: FieldIndex -> [FieldIndex]
|
||||||
|
entireAreaOf (outer, _) = do
|
||||||
|
innerRow <- enumerate
|
||||||
|
innerColumn <- enumerate
|
||||||
|
pure (outer, (innerRow, innerColumn))
|
||||||
|
|
||||||
|
affectedFieldsOf :: FieldIndex -> Set FieldIndex
|
||||||
|
affectedFieldsOf = Representable.index affectedFields
|
||||||
|
|
||||||
|
affectedFields :: Sudoku (Set FieldIndex)
|
||||||
|
affectedFields = tabulate @Sudoku (Set.fromList . allAffectedFieldsOf)
|
||||||
|
where
|
||||||
|
allAffectedFieldsOf :: FieldIndex -> [FieldIndex]
|
||||||
|
allAffectedFieldsOf position = List.concatMap ($ position) [entireRowOf, entireColumnOf, entireAreaOf]
|
||||||
|
|
41
src/Sudoku/Area.hs
Normal file
41
src/Sudoku/Area.hs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{-# LANGUAGE DerivingStrategies #-}
|
||||||
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
|
{-# LANGUAGE UndecidableInstances #-}
|
||||||
|
{-# LANGUAGE InstanceSigs #-}
|
||||||
|
{-# LANGUAGE DeriveTraversable #-}
|
||||||
|
module Sudoku.Area (Area(..), indexFromDigit) where
|
||||||
|
|
||||||
|
import Sudoku.Triple (Triple, Index)
|
||||||
|
import Data.Functor.Compose (Compose(..))
|
||||||
|
import Data.Functor.Rep (Representable)
|
||||||
|
import Data.Distributive (Distributive (..))
|
||||||
|
import Data.Bifunctor (Bifunctor(..))
|
||||||
|
import Control.Monad (join)
|
||||||
|
|
||||||
|
newtype Area a = Area { get :: Compose Triple Triple a }
|
||||||
|
deriving stock (Foldable, Traversable)
|
||||||
|
deriving newtype (Functor, Representable, Show)
|
||||||
|
|
||||||
|
instance Distributive Area where
|
||||||
|
distribute :: Functor f => f (Area a) -> Area (f a)
|
||||||
|
distribute = Area . distribute . fmap get
|
||||||
|
|
||||||
|
indexFromDigit :: Int -> (Index, Index)
|
||||||
|
indexFromDigit i = join bimap toEnum $ i `divMod` 3
|
||||||
|
|
||||||
|
digitFromIndex :: (Index, Index) -> Int
|
||||||
|
digitFromIndex (outer, inner) = fromEnum outer * 3 + fromEnum inner
|
||||||
|
|
||||||
|
-- prop> \ i -> digitFromIndex (indexFromDigit (i `mod` 9)) == i `mod` 9
|
||||||
|
-- +++ OK, passed 100 tests.
|
||||||
|
|
||||||
|
-- >>> import Sudoku.Triple (Index(..))
|
||||||
|
-- >>> digitFromIndex (First, First)
|
||||||
|
-- 0
|
||||||
|
--
|
||||||
|
-- >>> indexFromDigit 9
|
||||||
|
-- *** Exception: Enum.toEnum{Index}: tag (3) is outside of enumeration's range (0,2)
|
||||||
|
--
|
||||||
|
-- >>> indexFromDigit 6
|
||||||
|
-- (Third,First)
|
||||||
|
|
31
src/Sudoku/Field.hs
Normal file
31
src/Sudoku/Field.hs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
module Sudoku.Field (Field(..), parse, toDigit, maybeToDigit) where
|
||||||
|
import qualified Text.Megaparsec.Char as MegaParsec
|
||||||
|
import Text.Megaparsec (Parsec)
|
||||||
|
import Data.Void (Void)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Char as Char
|
||||||
|
|
||||||
|
data Field
|
||||||
|
= One
|
||||||
|
| Two
|
||||||
|
| Three
|
||||||
|
| Four
|
||||||
|
| Five
|
||||||
|
| Six
|
||||||
|
| Seven
|
||||||
|
| Eight
|
||||||
|
| Nine
|
||||||
|
deriving (Show, Ord, Eq, Enum, Bounded)
|
||||||
|
|
||||||
|
parse :: Parsec Void Text (Maybe Field)
|
||||||
|
parse = do
|
||||||
|
c <- MegaParsec.digitChar
|
||||||
|
pure $ case c of
|
||||||
|
'0' -> Nothing
|
||||||
|
_ -> Just $ toEnum (Char.digitToInt c - 1)
|
||||||
|
|
||||||
|
toDigit :: Field -> Char
|
||||||
|
toDigit = Char.intToDigit . (+1) . fromEnum
|
||||||
|
|
||||||
|
maybeToDigit :: Maybe Field -> Char
|
||||||
|
maybeToDigit = maybe '_' toDigit
|
35
src/Sudoku/Render.hs
Normal file
35
src/Sudoku/Render.hs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
module Sudoku.Render (unstructured, hints) where
|
||||||
|
import Sudoku (Sudoku, orderedIndices, translateIntIndex)
|
||||||
|
import Sudoku.Field (Field)
|
||||||
|
import qualified Sudoku.Field as Field
|
||||||
|
import qualified Data.Functor.Rep as Representable
|
||||||
|
import Data.Set (Set)
|
||||||
|
import Graphics.Text.Box (Box)
|
||||||
|
import qualified Graphics.Text.Box as Box
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
import Data.Text (Text)
|
||||||
|
|
||||||
|
unstructured :: Sudoku (Maybe Field) -> String
|
||||||
|
unstructured sudoku = let
|
||||||
|
characters = Field.maybeToDigit <$> sudoku
|
||||||
|
|
||||||
|
in unlines $ (Representable.index characters <$>) <$> orderedIndices
|
||||||
|
|
||||||
|
hints :: Sudoku (Either Field (Set Field)) -> Text
|
||||||
|
hints sudoku = let
|
||||||
|
|
||||||
|
boxes = fieldBox <$> sudoku :: Sudoku Box
|
||||||
|
|
||||||
|
in Box.render $ Box.asciiGrid (9, 9) (Representable.index boxes . Sudoku.translateIntIndex)
|
||||||
|
|
||||||
|
fieldBox :: Either Field (Set Field) -> Box
|
||||||
|
fieldBox = \case
|
||||||
|
Left value -> Box.square 3 (const $ Field.toDigit value)
|
||||||
|
Right options -> Box.square 3 $ \ (row, col) -> let
|
||||||
|
option = toEnum $ row * 3 + col
|
||||||
|
|
||||||
|
in if option `Set.member` options
|
||||||
|
then Field.toDigit option
|
||||||
|
else ' '
|
||||||
|
|
58
src/Sudoku/Solve.hs
Normal file
58
src/Sudoku/Solve.hs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
{-# LANGUAGE RequiredTypeArguments #-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
module Sudoku.Solve (solve, SolveError, hints) where
|
||||||
|
import Sudoku (Sudoku)
|
||||||
|
import Sudoku.Field (Field)
|
||||||
|
import qualified Sudoku.State as SudokuState
|
||||||
|
import Sudoku.State (SudokuState)
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
import qualified Data.Functor.Representable as Representable
|
||||||
|
import Data.Set ( Set )
|
||||||
|
import Data.Functor.Rep (imapRep)
|
||||||
|
|
||||||
|
data SolveError
|
||||||
|
= InvalidStartState
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
-- | Given a state implementation and a sudoku. Calculate all the hints
|
||||||
|
|
||||||
|
hints :: forall s -> SudokuState s => Sudoku (Maybe Field) -> Sudoku (Either Field (Set Field))
|
||||||
|
hints s sudoku = let
|
||||||
|
state = SudokuState.build @s sudoku
|
||||||
|
|
||||||
|
getHint position = \case
|
||||||
|
Just x -> Left x -- field is already filled
|
||||||
|
Nothing -> Right $ SudokuState.fieldOptions state position
|
||||||
|
|
||||||
|
in imapRep getHint sudoku
|
||||||
|
|
||||||
|
-- | Given a State implementation and a sudoku field, try to solve it.
|
||||||
|
|
||||||
|
solve :: forall s -> SudokuState s => Sudoku (Maybe Field) -> Either SolveError [Sudoku Field]
|
||||||
|
solve s sudoku = let
|
||||||
|
state = SudokuState.build @s sudoku
|
||||||
|
in case SudokuState.isValid state of
|
||||||
|
False -> Left InvalidStartState
|
||||||
|
True -> Right $ solveWithState s sudoku state
|
||||||
|
|
||||||
|
solveWithState :: forall s -> SudokuState s => Sudoku (Maybe Field) -> s -> [Sudoku Field]
|
||||||
|
solveWithState s sudoku state = case sequence sudoku of
|
||||||
|
Just solution -> [solution]
|
||||||
|
Nothing -> fillField s sudoku state
|
||||||
|
|
||||||
|
fillField :: forall s -> SudokuState s => Sudoku (Maybe Field) -> s -> [Sudoku Field]
|
||||||
|
fillField s sudoku state = case SudokuState.nextField state of
|
||||||
|
Nothing -> [] -- unsolvable
|
||||||
|
Just position -> do
|
||||||
|
-- list monad threads the execution
|
||||||
|
fieldValue <- Set.toList (SudokuState.fieldOptions state position)
|
||||||
|
|
||||||
|
-- update the stuff
|
||||||
|
let newBoard = Representable.set position (Just fieldValue) sudoku
|
||||||
|
let newState = SudokuState.setField state position fieldValue
|
||||||
|
|
||||||
|
-- recurse
|
||||||
|
solveWithState s newBoard newState
|
||||||
|
|
117
src/Sudoku/State.hs
Normal file
117
src/Sudoku/State.hs
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
{-# LANGUAGE NoFieldSelectors #-}
|
||||||
|
{-# LANGUAGE InstanceSigs #-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE TupleSections #-}
|
||||||
|
module Sudoku.State (SudokuState(..), State) where
|
||||||
|
|
||||||
|
import Control.Arrow ((>>>))
|
||||||
|
|
||||||
|
import Data.Bifunctor (Bifunctor(bimap))
|
||||||
|
import Data.Enum.Util (enumerate)
|
||||||
|
import Data.Functor.Rep (imapRep)
|
||||||
|
import Data.Set (Set)
|
||||||
|
import Data.Map (Map)
|
||||||
|
import Data.Maybe (mapMaybe, catMaybes, fromMaybe)
|
||||||
|
|
||||||
|
import qualified Data.Foldable as Foldable
|
||||||
|
import qualified Data.Functor.Rep as Representable
|
||||||
|
import qualified Data.Functor.Representable as Representable
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.Maybe as Maybe
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
import Sudoku (Sudoku, FieldIndex, entireAreaOf, entireRowOf, entireColumnOf, affectedFieldsOf)
|
||||||
|
import Sudoku.Field (Field)
|
||||||
|
|
||||||
|
class SudokuState a where
|
||||||
|
build :: Sudoku (Maybe Field) -> a
|
||||||
|
isValid :: a -> Bool
|
||||||
|
nextField :: a -> Maybe FieldIndex
|
||||||
|
fieldOptions :: a -> FieldIndex -> Set Field
|
||||||
|
setField :: a -> FieldIndex -> Field -> a
|
||||||
|
|
||||||
|
data State = State
|
||||||
|
{ fieldOptions :: Sudoku (Maybe (Set Field))
|
||||||
|
, optionCounts :: Map Int (Set FieldIndex)
|
||||||
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
collectConstraints :: Sudoku (Maybe Field) -> FieldIndex -> Maybe Field -> Maybe (Set Field)
|
||||||
|
collectConstraints sudoku position = \case
|
||||||
|
Just _ -> Nothing
|
||||||
|
Nothing -> Just $ let
|
||||||
|
collectPosition = Set.fromList . mapMaybe (Representable.index sudoku)
|
||||||
|
|
||||||
|
areaConstraints = collectPosition $ Sudoku.entireAreaOf position
|
||||||
|
rowConstraints = collectPosition $ Sudoku.entireRowOf position
|
||||||
|
columnConstraints = collectPosition $ Sudoku.entireColumnOf position
|
||||||
|
|
||||||
|
in Set.unions [areaConstraints, rowConstraints, columnConstraints]
|
||||||
|
|
||||||
|
collectOptions :: Sudoku (Maybe Field) -> FieldIndex -> Maybe Field -> Maybe (Set Field)
|
||||||
|
collectOptions sudoku position fieldValue = (Set.fromList enumerate `Set.difference`) <$> collectConstraints sudoku position fieldValue
|
||||||
|
|
||||||
|
instance SudokuState State where
|
||||||
|
build :: Sudoku (Maybe Field) -> State
|
||||||
|
build sudoku = let
|
||||||
|
|
||||||
|
options = imapRep (collectOptions sudoku) sudoku
|
||||||
|
counts = imapRep (\ pos -> fmap (, pos)) -- Sudoku (Maybe (FieldIndex, Set Field))
|
||||||
|
>>> catMaybes . Foldable.toList -- keep only non-present types
|
||||||
|
>>> fmap (bimap Set.size Set.singleton) -- set keys and values up
|
||||||
|
>>> Map.fromListWith Set.union -- combine them all
|
||||||
|
$ options
|
||||||
|
|
||||||
|
in State options counts
|
||||||
|
|
||||||
|
isValid :: State -> Bool
|
||||||
|
isValid (State options counts) = let
|
||||||
|
optionsValid = all (maybe True (not . Set.null)) options
|
||||||
|
countsValid = Maybe.isNothing (Map.lookup 0 counts)
|
||||||
|
|
||||||
|
in optionsValid && countsValid
|
||||||
|
|
||||||
|
nextField :: State -> Maybe FieldIndex
|
||||||
|
nextField (State _ counts) = Map.lookupMin counts >>= Set.lookupMin . snd
|
||||||
|
|
||||||
|
fieldOptions :: State -> FieldIndex -> Set Field
|
||||||
|
fieldOptions (State options _) = fromMaybe Set.empty . Representable.index options
|
||||||
|
|
||||||
|
setField :: State -> FieldIndex -> Field -> State
|
||||||
|
setField (State options counts) position value = let
|
||||||
|
|
||||||
|
affectedFields = affectedFieldsOf position
|
||||||
|
|
||||||
|
newOptions = Representable.set position Nothing -- remove the field where the value was set
|
||||||
|
>>> adjustAffectedOptions affectedFields value -- remove a lot of options in affected fields
|
||||||
|
$ options
|
||||||
|
|
||||||
|
oldOptionsCount = maybe 0 Set.size $ Representable.index options position -- how many options did the field have before it was set?
|
||||||
|
newCounts = Map.adjust (Set.delete position) oldOptionsCount -- forget the set field
|
||||||
|
>>> adjustAffectedCounts options newOptions affectedFields
|
||||||
|
>>> Map.filter (not . Set.null) -- remove empty sets, they would interfere with future operations
|
||||||
|
$ counts
|
||||||
|
|
||||||
|
in State newOptions newCounts
|
||||||
|
|
||||||
|
adjustAffectedCounts :: Sudoku (Maybe (Set Field)) -> Sudoku (Maybe (Set Field)) -> Set FieldIndex -> Map Int (Set FieldIndex) -> Map Int (Set FieldIndex)
|
||||||
|
adjustAffectedCounts newOptions oldOptions affectedFields counts = Set.foldl' (adjustCount newOptions oldOptions) counts affectedFields
|
||||||
|
|
||||||
|
adjustCount :: Sudoku (Maybe (Set Field)) -> Sudoku (Maybe (Set Field)) -> Map Int (Set FieldIndex) -> FieldIndex -> Map Int (Set FieldIndex)
|
||||||
|
adjustCount oldOptions newOptions counts position = let
|
||||||
|
oldOptionCount = maybe 0 Set.size $ Representable.index oldOptions position
|
||||||
|
newOptionCount = maybe 0 Set.size $ Representable.index newOptions position
|
||||||
|
|
||||||
|
in Map.adjust (Set.delete position) oldOptionCount
|
||||||
|
>>> Map.adjust (Set.insert position) newOptionCount
|
||||||
|
$ counts
|
||||||
|
|
||||||
|
adjustAffectedOptions :: Set FieldIndex -> Field -> Sudoku (Maybe (Set Field)) -> Sudoku (Maybe (Set Field))
|
||||||
|
adjustAffectedOptions fields value = imapRep adjustOptionField
|
||||||
|
where
|
||||||
|
adjustOptionField i
|
||||||
|
| i `Set.member` fields = fmap (Set.delete value)
|
||||||
|
| otherwise = id
|
||||||
|
-- Set.foldl' (removeOption value) options fields: Folding took a lot of time because of immutability
|
||||||
|
|
||||||
|
|
48
src/Sudoku/Triple.hs
Normal file
48
src/Sudoku/Triple.hs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{-# LANGUAGE DeriveTraversable #-}
|
||||||
|
{-# LANGUAGE InstanceSigs #-}
|
||||||
|
{-# LANGUAGE DerivingVia #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
module Sudoku.Triple (Triple(..), Index(..), first, second, third) where
|
||||||
|
import Data.Distributive (Distributive (distribute, collect))
|
||||||
|
import Data.Functor.Rep
|
||||||
|
( Representable(Rep, tabulate, index), Co, Co(..) )
|
||||||
|
|
||||||
|
data Triple a = Triple a a a
|
||||||
|
deriving stock (Show, Eq, Functor, Foldable, Traversable)
|
||||||
|
deriving (Applicative, Monad) via Co Triple
|
||||||
|
|
||||||
|
first :: Triple a -> a
|
||||||
|
first (Triple x _ _) = x
|
||||||
|
second :: Triple a -> a
|
||||||
|
second (Triple _ x _) = x
|
||||||
|
third :: Triple a -> a
|
||||||
|
third (Triple _ _ x) = x
|
||||||
|
|
||||||
|
instance Distributive Triple where
|
||||||
|
distribute :: Functor f => f (Triple a) -> Triple (f a)
|
||||||
|
distribute x = Triple (fmap first x) (fmap second x) (fmap third x)
|
||||||
|
collect :: Functor f => (a -> Triple b) -> f a -> Triple (f b)
|
||||||
|
collect f x = distribute $ fmap f x
|
||||||
|
|
||||||
|
data Index = First | Second | Third
|
||||||
|
deriving (Show, Eq, Ord, Enum, Bounded)
|
||||||
|
|
||||||
|
-- >>> toEnum 0 :: Index
|
||||||
|
-- First
|
||||||
|
|
||||||
|
instance Representable Triple where
|
||||||
|
type Rep Triple = Index
|
||||||
|
|
||||||
|
tabulate :: (Rep Triple -> a) -> Triple a
|
||||||
|
tabulate f = Triple (f First) (f Second) (f Third)
|
||||||
|
|
||||||
|
index :: Triple a -> Rep Triple -> a
|
||||||
|
index (Triple x1 x2 x3) = \case
|
||||||
|
First -> x1
|
||||||
|
Second -> x2
|
||||||
|
Third -> x3
|
||||||
|
|
||||||
|
-- >>> tabulate (const $ tabulate id) :: Triple (Triple Index)
|
||||||
|
-- Triple (Triple First Second Third) (Triple First Second Third) (Triple First Second Third)
|
||||||
|
|
67
stack.yaml
Normal file
67
stack.yaml
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# This file was automatically generated by 'stack init'
|
||||||
|
#
|
||||||
|
# Some commonly used options have been documented as comments in this file.
|
||||||
|
# For advanced use and comprehensive documentation of the format, please see:
|
||||||
|
# https://docs.haskellstack.org/en/stable/configure/yaml/
|
||||||
|
|
||||||
|
# A 'specific' Stackage snapshot or a compiler version.
|
||||||
|
# A snapshot resolver dictates the compiler version and the set of packages
|
||||||
|
# to be used for project dependencies. For example:
|
||||||
|
#
|
||||||
|
# snapshot: lts-23.24
|
||||||
|
# snapshot: nightly-2025-06-15
|
||||||
|
# snapshot: ghc-9.8.4
|
||||||
|
#
|
||||||
|
# The location of a snapshot can be provided as a file or url. Stack assumes
|
||||||
|
# a snapshot provided as a file might change, whereas a url resource does not.
|
||||||
|
#
|
||||||
|
# snapshot: ./custom-snapshot.yaml
|
||||||
|
# snapshot: https://example.com/snapshots/2024-01-01.yaml
|
||||||
|
snapshot: nightly-2025-07-09
|
||||||
|
compiler: ghc-9.12.1
|
||||||
|
|
||||||
|
# User packages to be built.
|
||||||
|
# Various formats can be used as shown in the example below.
|
||||||
|
#
|
||||||
|
# packages:
|
||||||
|
# - some-directory
|
||||||
|
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
|
||||||
|
# subdirs:
|
||||||
|
# - auto-update
|
||||||
|
# - wai
|
||||||
|
packages:
|
||||||
|
- .
|
||||||
|
# Dependency packages to be pulled from upstream that are not in the snapshot.
|
||||||
|
# These entries can reference officially published versions as well as
|
||||||
|
# forks / in-progress versions pinned to a git hash. For example:
|
||||||
|
#
|
||||||
|
# extra-deps:
|
||||||
|
# - acme-missiles-0.3
|
||||||
|
# - git: https://github.com/commercialhaskell/stack.git
|
||||||
|
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
|
||||||
|
#
|
||||||
|
# extra-deps: []
|
||||||
|
|
||||||
|
# Override default flag values for project packages and extra-deps
|
||||||
|
# flags: {}
|
||||||
|
|
||||||
|
# Extra package databases containing global packages
|
||||||
|
# extra-package-dbs: []
|
||||||
|
|
||||||
|
# Control whether we use the GHC we find on the path
|
||||||
|
# system-ghc: true
|
||||||
|
#
|
||||||
|
# Require a specific version of Stack, using version ranges
|
||||||
|
# require-stack-version: -any # Default
|
||||||
|
# require-stack-version: ">=3.7"
|
||||||
|
#
|
||||||
|
# Override the architecture used by Stack, especially useful on Windows
|
||||||
|
# arch: i386
|
||||||
|
# arch: x86_64
|
||||||
|
#
|
||||||
|
# Extra directories used by Stack for building
|
||||||
|
# extra-include-dirs: [/path/to/dir]
|
||||||
|
# extra-lib-dirs: [/path/to/dir]
|
||||||
|
#
|
||||||
|
# Allow a newer minor version of GHC than the snapshot specifies
|
||||||
|
# compiler-check: newer-minor
|
12
stack.yaml.lock
Normal file
12
stack.yaml.lock
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# This file was autogenerated by Stack.
|
||||||
|
# You should not edit this file by hand.
|
||||||
|
# For more information, please see the documentation at:
|
||||||
|
# https://docs.haskellstack.org/en/stable/topics/lock_files
|
||||||
|
|
||||||
|
packages: []
|
||||||
|
snapshots:
|
||||||
|
- completed:
|
||||||
|
sha256: 193b933e68724f1cd2b266649318f8c102f6f6d7a52415a8eee8dc604fb5d6b2
|
||||||
|
size: 723871
|
||||||
|
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2025/7/9.yaml
|
||||||
|
original: nightly-2025-07-09
|
2
test/Spec.hs
Normal file
2
test/Spec.hs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
main :: IO ()
|
||||||
|
main = putStrLn "Test suite not yet implemented"
|
9
test/example.s3
Normal file
9
test/example.s3
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
003020600
|
||||||
|
900305001
|
||||||
|
001806400
|
||||||
|
008102900
|
||||||
|
700000008
|
||||||
|
006708200
|
||||||
|
002609500
|
||||||
|
800203009
|
||||||
|
005010300
|
Loading…
Add table
Add a link
Reference in a new issue