Skip to content

Commit

Permalink
added regular expression test to yaderConf; version up
Browse files Browse the repository at this point in the history
  • Loading branch information
nineclue committed Feb 2, 2024
1 parent 73d54a0 commit 7682840
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 34 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ThisBuild / scalaVersion := "3.3.1"
ThisBuild / version := "0.9.1"
ThisBuild / version := "0.9.2"
ThisBuild / organization := "net.maryknollrad"

val catsEffectVersion = "3.5.1"
Expand Down
46 changes: 33 additions & 13 deletions cli/src/main/scala/Console.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,44 @@ import cats.effect.*
import cats.syntax.all.*

object Console:
def nonEmpty(s: String) = if s.trim().isEmpty then Some("Answer should not be empty") else None
type ConsValidate = String => Either[String, String]

def nonEmpty(s: String) = if s.isEmpty then Left("Answer should not be empty") else Right(s)

private val ip = raw"(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})".r
def isIPAddr(s: String) = s match
case ip(a, b, c, d) if a.toInt <= 255 && b.toInt <= 255 && c.toInt <= 255 && d.toInt <= 255 => None
case _ => Some(s"IP address is not in correct format : $s")
case ip(a, b, c, d) if a.toInt <= 255 && b.toInt <= 255 && c.toInt <= 255 && d.toInt <= 255 => Right(s)
case _ => Left(s"IP address is not in correct format : $s")

def isNumber(s: String) = scala.util.Try(s.toInt).toOption match
case Some(_) => None
case _ => Some("Number should be given")
case Some(_) => Right(s)
case _ => Left("Number should be given")

def numberInRange(r: Range)(s: String) = scala.util.Try(s.toInt).toOption match
case Some(n) if r.contains(n) => None
case _ => Some(s"Number should be given and in range between ${r.start} and ${r.end+1}")
case Some(n) if r.contains(n) => Right(s)
case _ => Left(s"Number should be given and in range between ${r.start} and ${r.end+1}")

def lengthInRange(r: Range)(s: String) =
if r.contains(s.length) then None
else Some(s"String too long. Should be less than or equal to ${r.end}")
if r.contains(s.length) then Right(s)
else Left(s"String too long or short. Should be in range between ${r.start} and${r.end}")

def oneOf(choices: Seq[String]) =
val lowCs = choices.withFilter(_.trim().nonEmpty).map(_.toLowerCase())
val cSample = lowCs.map(c =>
val rest = if c.tail.isEmpty then "" else s"[${c.tail}]"
s"${c.head}$rest")
assert(cSample.length > 1)
val choiceSample = cSample.init.mkString(",") ++ s" or ${cSample.last}"
(s: String) =>
nonEmpty(s).flatMap(ss =>
val lowS = ss.toLowerCase()
lowCs.find(c => c == lowS || c.head == lowS.head) match
case Some(_) => Right(s)
case _ => Left(s"Please enter $choiceSample"))

def yesOrNo = oneOf(Seq("yes", "no"))

def printAndRead(msg: String, validateFunction: Option[String => Option[String]] = None,
def printAndRead(msg: String, validateFunction: Option[ConsValidate] = None,
default: Option[String] = None): IO[String] =
val defaultAppended = default.map(d => s"$msg [$d] : ").getOrElse(msg ++ " : ")
val h =
Expand All @@ -30,9 +48,11 @@ object Console:
ans <- IO.readLine
yield ans
def r: IO[String] = h.flatMap(s =>
val amended = if default.nonEmpty && s.trim().isEmpty() then default.get else s
val amended = default match
case Some(d) if s.trim().isEmpty() => d
case _ => s.trim()
validateFunction.map(f => f(amended) match
case None => IO.pure(amended)
case Some(msg) => IO.println(msg) *> r
case Right(s) => IO.pure(s)
case Left(msg) => IO.println(msg) *> r
).getOrElse(IO.pure(amended)))
r
102 changes: 82 additions & 20 deletions cli/src/main/scala/YaderConf.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import net.maryknollrad.d4cs.*
import javax.imageio.ImageIO
import org.dcm4che3.io.DicomInputStream
import net.sourceforge.tess4j.*
import org.apache.commons.text.StringEscapeUtils
import org.apache.commons.text.{StringEscapeUtils => SEU}

object YaderConf extends IOApp:
private def config(host: String, port: Int, called: String, calling: String, encoding: String, tpath: String, insts: Seq[String]) =
private def config(host: String, port: Int, called: String, calling: String, encoding: String,
tpath: String, insts: Seq[String], rgx: Option[String]) =
s"""host = "$host"
|port = $port
|called-ae = "$called"
Expand All @@ -24,7 +25,7 @@ object YaderConf extends IOApp:
|# postgres-password = ""
|
|# install path of tesseract
|tesseract-path = "${StringEscapeUtils.escapeJava(tpath)}"
|tesseract-path = "${SEU.escapeJava(tpath)}"
|# to filter other hospital's exam, multiple string values are supported
|institution = [${insts.map(inst => s"\"$inst\"").mkString(",")}]
|# dose value is ct or DLP
Expand Down Expand Up @@ -59,8 +60,8 @@ object YaderConf extends IOApp:
|show-none = false
|
|# set default drl category, first category if not specified
|# default-drl-category = "korea"
|""".stripMargin
|# default-drl-category = "korea"
|""".stripMargin ++ rgx.map(r => s"# your regular expression : '${SEU.escapeJava(r)}'").getOrElse("")

private def printHello =
IO.println("Yader configuration utility\n\n")
Expand Down Expand Up @@ -109,14 +110,20 @@ object YaderConf extends IOApp:

private val tess = new Tesseract()
private val windows = "windows.*".r
private val winTess = "C:\\Program Files\\Tesseract-OCR"
private def winTessPath =
val drives = Seq("C:", "D:", "E:")
val paths = Seq("\\Program Files\\Tesseract-OCR", "\\Tesseract-OCR")
drives.flatMap(d => paths.map(p => d ++ p))
.find(path => { val p = os.Path(path); os.exists(p) && os.isDir(p) })
private val mac = "mac.*".r
private val macBrewTess = "/opt/homebrew/Cellar/tesseract/5.3.3"
private def macBrewTessPath =
val tp = os.Path("/opt/homebrew/Cellar/tesseract")
Option.when(os.exists(tp))(os.list(tp).filter(os.isDir).sorted.last).map(_.toString)
private val linux = "linux.*".r
private def findTesseract =
System.getProperty("os.name").toLowerCase() match
case windows() if os.exists(os.Path(winTess)) => Some(winTess)
case mac() if os.exists(os.Path(macBrewTess)) => Some(macBrewTess)
case windows() => winTessPath
case mac() => macBrewTessPath
case _ => None

private def saveAndOCR(i: Int, ds: Tuple2[DicomInputStream, DicomInputStream], tp: Option[String] = None) =
Expand All @@ -143,6 +150,51 @@ object YaderConf extends IOApp:
private val unsuccessfulPing = "PING failed.\nServer is not reachable or the sever did not respond."
private def printline = IO.println("-" * 80)

import scala.util.matching.Regex, Regex.Match
private def findFirstDoubleStringInMatch(m: Match) =
m.subgroups.flatMap(s => scala.util.Try(s.toDouble).toOption).headOption

private def testRegex(texts: Seq[String]) =
val getrgx = printAndRead("Enter regular expression to apply", Some(nonEmpty))
val applyAndFilter = (s: String) =>
val rgx = scala.util.matching.Regex(s).unanchored
texts.map(rgx.findFirstMatchIn).zip(texts.zipWithIndex)
.map(t3 => (t3._2._2, t3._2._1, t3._1))
val printResults = (rs: Seq[(Int, String, Option[Match])]) => rs.traverse :
case (i, txt, Some(m)) =>
val matched =
if m.subgroups.isEmpty then
"No matching text found"
else
m.subgroups.mkString(s"Matched ${m.subgroups.length} strings : [\"", "\", \"", "\"]")
val numValue =
findFirstDoubleStringInMatch(m).map(d => s"Found number : $d").getOrElse("Could not find number.")
IO.println(s"$i)") *> IO.println(txt)
*> printline
*> IO.println(matched)
*> IO.println(numValue)
case _ =>
IO.println("Unsuccessful matching.")
def run: IO[Option[String]] =
for
rgx <- getrgx
results = applyAndFilter(rgx)
_ <- printResults(results)
yOrN <- printAndRead("Is it correctly matched ? (Yes/No/Quit)", Some(oneOf(Seq("yes", "no", "quit"))))
r <- yOrN match
case "n" | "no" => run
case "y" | "yes" => IO.some(rgx)
case "q" | "quit" => IO.none
case _ => run
yield r
run

private def split[A, B](s: Seq[(A, Option[B])], acc: (Seq[A], Seq[Option[B]])): (Seq[A], Seq[Option[B]]) =
if s.isEmpty then acc
else
val h = s.head
split(s.tail, (acc._1 :+ h._1, if h._2.isEmpty then acc._2 else acc._2 :+ h._2))

private def runTestsAndSave(host: String, port: Int, called: String, calling: String, encoding: String) =
DicomBase.resources(host, port, called, calling, encoding, false).use :
case (cfind, cget) =>
Expand Down Expand Up @@ -173,22 +225,32 @@ object YaderConf extends IOApp:
case (dcm, i) => saveAndOCR(i, dcm, otpath).flatMap({
_ match
case Some((ans, oinst)) =>
printline *> IO.println("Result") *> IO.println(ans) *> printline
*> IO.pure(oinst)
printline *> IO.println(s"Result ($i)") *> IO.println(ans)
*> printline
*> IO.some((ans, oinst))
case _ =>
IO.pure(None)
IO.none
})
files = if ocrs.length == 1 then
"1 file. (dose0.png)"
else
s"${ocrs.length} files. (dose0.png - dose${ocrs.length-1}.png)"
insts = ocrs.flatten.distinct.sorted
_ <- printline *> IO.println(s"Stored $files")
succeeded = ocrs.flatten
files = succeeded.length match
case 0 => "0 file."
case 1 => "1 file. (dose0.png)"
case _ => s"${ocrs.length} files. (dose0.png - dose${ocrs.length-1}.png)"
_ <- printline *> IO.println(s"Stored $files")
(texts, insts) = split(succeeded, (Seq.empty, Seq.empty))
rgx <- if succeeded.length > 0 then
printAndRead("Do you want to check regular expression for ct-info.conf (Yes/No) ?", Some(yesOrNo)).flatMap(_ match
case "yes" | "y" => testRegex(texts)
case _ => IO.none
) else
IO.none
_ <- otpath match
case Some(tp) =>
IO(os.write.over(os.pwd / "yader.conf", config(host, port, called, calling, encoding, tp, insts)))
IO(os.write.over(os.pwd / "yader.conf", config(host, port, called, calling, encoding, tp, insts.flatten, rgx)))
*> printline *> IO.println("Saved yader.conf file.")
case _ => IO.unit
case _ =>
IO.println("Failed to save config file. Please check Tesseract install path.") *>
IO.unit
yield () }
case false =>
IO.println(unsuccessfulPing)
Expand Down

0 comments on commit 7682840

Please sign in to comment.