redone on cotlinx-cli, added writing and reading to json

This commit is contained in:
ZhigalskiiIvan 2023-03-31 20:11:59 +03:00
parent 9ce3f34c1f
commit d7cea6ac71
2 changed files with 188 additions and 240 deletions

View File

@ -19,6 +19,7 @@ dependencies {
implementation("org.slf4j:slf4j-nop:1.7.29") implementation("org.slf4j:slf4j-nop:1.7.29")
implementation("org.jetbrains.lets-plot:lets-plot-image-export:3.1.0") implementation("org.jetbrains.lets-plot:lets-plot-image-export:3.1.0")
testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'org.jetbrains.kotlin:kotlin-test'
implementation 'com.google.code.gson:gson:2.8.9'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
} }

View File

@ -1,55 +1,26 @@
@file:OptIn(ExperimentalCli::class)
import java.io.File import java.io.File
import kotlin.system.exitProcess import kotlin.system.exitProcess
import org.jetbrains.letsPlot.* import org.jetbrains.letsPlot.*
import org.jetbrains.letsPlot.export.ggsave import org.jetbrains.letsPlot.export.ggsave
import org.jetbrains.letsPlot.geom.* import org.jetbrains.letsPlot.geom.*
import kotlinx.cli.* import kotlinx.cli.*
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import java.io.FileReader
/** Recognizes the name of the text, which val WHITESPACES = Regex("\\s+")
* user want to see statistics about and type of output.
* It calls methods of graphic building or printing data in console,
* build required for it objects.
*/
fun getStatistics(textData: TextData) {
if (!textData.haveText()) {
println("No saved texts")
return
}
val text = textData.getTextData("Input name of a text which you want to see statistics about.")
var counter = 1
val wordsCountsMap = text.getSentencesList().map { counter++ to it.getWordsCount() }
println(
"Print \"console\" if you have see data in console, \"graphic\" if you have see histogram \n" +
"and \"both\" if you have see them together:"
)
val request = requestInput(listOf("console", "graphic", "both"))
request.first.exe()
when (request.second) {
"console" -> printStatisticsInConsole(text.getName(), wordsCountsMap.toMap())
"graphic" -> buildGraphic(text.getName(), wordsCountsMap.toMap())
"both" -> {
printStatisticsInConsole(text.getName(), wordsCountsMap.toMap())
buildGraphic(text.getName(), wordsCountsMap.toMap())
}
}
}
/** Builds bar chart with data of mapOfSentenceNumToItsSize and saves image in file. /** Builds bar chart with data of mapOfSentenceNumToItsSize and saves image in file.
* @param textName name of texts, which user want to see statistics about. * @param textName name of texts, which user want to see statistics about.
* @param mapOfSentenceNumToItsSize map of pairs of sentence numbers and their words count. * @param mapOfSentenceNumToItsSize map of pairs of sentence numbers and their words count.
*/ */
fun buildGraphic(textName: String, mapOfSentenceNumToItsSize: Map<Int, Int>) { fun buildGraphic(textName: String, listOfSentenceSizes: List<Int>) {
val data = mapOf( val data = mapOf("words count" to listOfSentenceSizes.map { it.toFloat() })
"words count" to mapOfSentenceNumToItsSize.map { it.value.toFloat() / mapOfSentenceNumToItsSize.size },
)
val fig = ggplot(data) + val fig = ggplot(data) +
geomBar( geomBar(
@ -66,7 +37,6 @@ fun buildGraphic(textName: String, mapOfSentenceNumToItsSize: Map<Int, Int>) {
println("Graphic was save in ${ggsave(fig, "$textName.png")}") println("Graphic was save in ${ggsave(fig, "$textName.png")}")
fig.show() fig.show()
} }
/** Prints statistics according to data from mapOfSentenceNumToItsSize. /** Prints statistics according to data from mapOfSentenceNumToItsSize.
@ -103,34 +73,11 @@ fun printStatisticsInConsole(textName: String, mapOfSentenceNumToItsSize: Map<In
} }
/** Asks the user which where they want to read the text from and
* calls special for file- and console- reading methods according the answer.
*/
fun readNewText(textData: TextData) {
println("Where do you want to add the text: from the console or a file?")
val kindOfSource = requestInput(listOf("console", "file"))
kindOfSource.first.exe()
when (kindOfSource.second) {
"console" -> readFromConsole(textData)
"file" -> readFromFile(textData)
}
}
/** Read from console the name of text, checks that it hasn't saved yet and its contents, /** Read from console the name of text, checks that it hasn't saved yet and its contents,
* asks if entered text is not correct and re-calls itself or * asks if entered text is not correct and re-calls itself or
* calls method of adding received text to data. * calls method of adding received text to data.
*/ */
fun readFromConsole(textData: TextData) { fun readFromConsole(textData: TextData, textName: String) {
println("Input name of a text:")
if (textData.haveText()) println("Unavailable(existing) names: ${textData.getTextNamesInString()}.")
val nameRequest = requestInput(unavailableInputs = textData.getTextNamesList())
nameRequest.first.exe()
val name = nameRequest.second!!
println("Input a text content(after input text press enter twice):") println("Input a text content(after input text press enter twice):")
var content = "" var content = ""
@ -150,54 +97,22 @@ fun readFromConsole(textData: TextData) {
} }
} }
println("Was input data right?[yes, no]:") addTextToData(textData, textName, content)
val correctInputRequest = requestInput(listOf("yes", "no"))
correctInputRequest.first.exe()
if (correctInputRequest.second == "yes") addTextToData(textData, name, content)
else readFromConsole(textData)
} }
/** Asks for the name of text, path to file to read text from, /** Asks for the name of text, path to file to read text from,
* checks for its existing, reads contents and, * checks for its existing, reads contents and,
* asks if entered names is not correct and re-calls itself or * asks if entered names is not correct and re-calls itself or
* calls method of adding received text to data. * calls method of adding received text to data.
*/ */
fun readFromFile(textData: TextData) { fun readFromFile(textData: TextData, textName: String, path: String) {
println("Input a name of a text:")
val name = readln()
println("Input path to file:") println("Input path to file:")
val contentsFile: File val contentsFile = File(path)
while (true) {
val pathRequest = requestInput()
pathRequest.first.exe()
val filePath = pathRequest.second!!
val testFile = File(filePath)
if (!testFile.exists()) {
println("Incorrect path. Repeat the input:")
continue
} else {
contentsFile = testFile
break
}
}
val content = contentsFile.readText() val content = contentsFile.readText()
print("Input was correct?[yes, no]: ") addTextToData(textData, textName, content)
val correctInputRequest = requestInput(listOf("yes", "no"))
correctInputRequest.first.exe()
when (correctInputRequest.second) {
"yes" -> addTextToData(textData, name, content)
"no" -> readFromFile(textData)
}
} }
/** /**
@ -216,32 +131,36 @@ fun addTextToData(textData: TextData, textName: String, content: String) =
class TextData { class TextData {
/** list of monitored texts. */ /** list of monitored texts. */
private val textsList = mutableListOf<Text>() private val textsList: MutableSet<Text>
fun haveText(): Boolean = textsList.isNotEmpty() init {
textsList = JSONReaderWriter.readSavedTexts()
}
private fun haveText(): Boolean = textsList.isNotEmpty()
/** Method for removing text from watch list. Asks for a name of a text /** Method for removing text from watch list. Asks for a name of a text
* and if it's being tracked, remove it from the textsList. * and if it's being tracked, remove it from the textsList.
*/ */
fun removeText() { fun removeText(name: String) {
if (!haveText()) { val removingText = getTextByName(name)
println("No saved texts") if (removingText == null) println("No text with name $name")
return else {
}
val removingText = getTextData("Input name of text which you want to remove.")
textsList.remove(removingText) textsList.remove(removingText)
println("Text ${removingText.getName()} removed.") println("Text ${removingText.getName()} removed.")
JSONReaderWriter.removeFromDirectory(name)
} }
}
/** Returns Text object whose name matches searchingName or null /** Returns Text object whose name matches searchingName or null
* if there is no text with same name in the textsList. * if there is no text with same name in the textsList.
* @param searchingName name of the text to be found. * @param searchingName name of the text to be found.
* @return the text with searchingName name or null. * @return the text with searchingName name or null.
*/ */
private fun getTextByName(searchingName: String): Text? { fun getTextByName(searchingName: String): Text? {
for (text in textsList) if (text.getName() == searchingName) return text for (text in textsList) if (text.getName() == searchingName) return text
return null return null
@ -263,26 +182,50 @@ class TextData {
* textName and content and adds it in textsList. * textName and content and adds it in textsList.
*/ */
fun addNewText(textName: String, content: String) { fun addNewText(textName: String, content: String) {
textsList.add(TextAnalyzer.getTextObjFromContents(textName, content)) val newText = TextAnalyzer.getTextObjFromContents(textName, content)
textsList.add(newText)
JSONReaderWriter.writeInJSON(newText)
} }
/** private object JSONReaderWriter {
* Reads name of the text to be found and returns it text if private val gsonPretty: Gson = GsonBuilder().setPrettyPrinting().create()
* it exists. private val savedTextsDirectory: File = File("savedTexts")
* @param message message which prints with calling this method. private val savedTextsFiles: Array<File>
* @return Text type object
*/
fun getTextData(message: String = "Input name of a text"): Text {
println(message)
println("Saved texts: ${getTextNamesInString()}.")
val nameRequest = requestInput(getTextNamesList()) init {
nameRequest.first.exe() if (!savedTextsDirectory.exists()) savedTextsDirectory.mkdir()
savedTextsFiles =
return getTextByName(nameRequest.second!!)!! savedTextsDirectory.listFiles { _, filename ->
filename.split(".").last() == "json"
} ?: emptyArray()
} }
fun removeFromDirectory(name: String) {
savedTextsFiles.find { it.name == name }?.delete() ?: println("No file with name $name in directory")
}
fun writeInJSON(text: Text) {
val jsonFileText = gsonPretty.toJson(text)
val jsonFile = File("${savedTextsDirectory.path}/${text.getName()}.json")
jsonFile.createNewFile()
jsonFile.writeText(jsonFileText)
}
fun readFromJSON(jsonFile: File): Text {
return gsonPretty.fromJson(FileReader(jsonFile), Text::class.java)
}
fun readSavedTexts(): MutableSet<Text> {
val textsList = mutableListOf<Text>()
for (file in savedTextsFiles) {
textsList.add(readFromJSON(file))
}
return textsList.toMutableSet()
}
}
/** Object used for getting text from the string information. */ /** Object used for getting text from the string information. */
private object TextAnalyzer { private object TextAnalyzer {
@ -330,13 +273,11 @@ class TextData {
fun getSentencesList() = sentencesList fun getSentencesList() = sentencesList
/** Stores information about count of words. */ /** Stores information about count of words. */
data class Sentence(private val wordsCount: Int) { data class Sentence(private val wordsCount: Int) {
fun getWordsCount() = wordsCount fun getWordsCount() = wordsCount
} }
} }
} }
@ -352,61 +293,125 @@ fun exit(): Nothing {
*/ */
class CommandCenter(private val textData: TextData) { class CommandCenter(private val textData: TextData) {
private val exitCommand = Command("exit", ::exit)
private val addCommand = Command("add text") { readNewText(textData) }
private val showStatisticsCommand = Command("show statistics") { getStatistics(textData) }
private val removeTextCommand = Command("remove text") { textData.removeText() }
private val commandsList = listOf(exitCommand, addCommand, showStatisticsCommand, removeTextCommand) private val parser = ArgParser(
private val commandsNames = commandsList.map { it.name } "Text Analyzer",
useDefaultHelpShortName = true,
strictSubcommandOptionsOrder = false
private val parser = ArgParser("Text Analyzer", useDefaultHelpShortName = true)
private val add by parser.option(
ArgType.String, "add", "a",
"adds new text to data", "Add warn"
)
private val remove by parser.option(
ArgType.String, "remove", "r",
"removes text from data", "Remove warn"
)
private val stat by parser.option(
ArgType.String, "statistics", "s",
"shows statistics", "Stat warn"
)
private val exit by parser.option(
ArgType.String, "exit", "e",
"exit from program", "exit warn"
) )
// fun parseCommand(args: Array<String>) {
// parser.parse(args) private val addNewText = Add()
// private val showStatistics = ShowStatistics()
// when (add) { private val showTextList = ShowTextsList()
// "" -> readNewText(textData) private val removeTexts = RemoveTexts()
// null -> {}
// else -> private inner class RemoveTexts : Subcommand("remove", "Removing text from saved.") {
// } private val removingList by argument(
// } ArgType.String, "removing list",
"Selection of texts to delete"
).vararg()
override fun execute() {
if (removingList.isEmpty()) {
println("remove command needs arguments to work")
return
}
val availableRemovingList = mutableListOf<String>()
removingList.forEach {
if (it in textData.getTextNamesList()) availableRemovingList.add(it)
else println("No text $it in data")
}
availableRemovingList.forEach { textData.removeText(it) }
}
}
private inner class ShowTextsList : Subcommand("list", "Showing tracking texts.") {
override fun execute() {
println("Saved texts list: ${textData.getTextNamesInString(", ")}")
}
}
/** Stores command and its name */ private inner class ShowStatistics : Subcommand("stat", "Showing statistics.") {
private class Command(val name: String, val executingFun: () -> Unit)
/** Prints list of names of available commands, requests the name and private val textNames by argument(
* returns corresponding to entered name function. ArgType.String, "text names",
*/ "Names of texts which you want to show statistics about"
fun readCommandFromConsole(): () -> Unit { ).vararg()
println("Input one of the commands: ${commandsNames.joinToString()}:") private val outputLocation by option(
ArgType.Choice(listOf("graphic", "console", "both"), { it }), "output-location", "o",
"Choice where to show the statistics"
).default("console")
val commandNameRequest = requestInput(commandsNames) override fun execute() {
commandNameRequest.first.exe()
val funName = commandNameRequest.second val availableSelectedTextNames = mutableListOf<String>()
textNames.forEach {
if (it in textData.getTextNamesList()) availableSelectedTextNames.add(it)
else println("No text $it in data")
}
return commandsList.find { it.name == funName }!!.executingFun when (outputLocation) {
"graphic" -> availableSelectedTextNames.forEach { callBuildingGraphic(it) }
"console" -> availableSelectedTextNames.forEach { callConsoleStatisticsShowing(it) }
"both" -> availableSelectedTextNames.forEach {
callBuildingGraphic(it)
callConsoleStatisticsShowing(it)
}
}
}
private fun callBuildingGraphic(textName: String) {
val text = textData.getTextByName(textName)!!
val listOfSentenceSizes = text.getSentencesList().map { it.getWordsCount() }
buildGraphic(textName, listOfSentenceSizes)
}
private fun callConsoleStatisticsShowing(textName: String) {
val text = textData.getTextByName(textName)!!
var counter = 1
val mapOfSentenceNumToItsSize = text.getSentencesList().associate { counter++ to it.getWordsCount() }
printStatisticsInConsole(textName, mapOfSentenceNumToItsSize)
}
}
private inner class Add : Subcommand("add", "Adding text from source") {
private val path by option(
ArgType.String, "source path", "s",
"Inputting path to file for reading or \"console\" to reading from console"
).default("console")
private var textName by argument(
ArgType.String, "name",
"Specifies the name of the new text"
)
override fun execute() {
if (textName == "") textName = "untitled"
when {
path == "console" -> callReadingFromConsole()
!File(path).exists() -> throw ReturnException("No file at $path")
else -> callReadingFromFile()
}
}
private fun callReadingFromConsole() = readFromConsole(textData, textName)
private fun callReadingFromFile() = readFromFile(textData, textName, path)
}
fun parseArgsAndExecute(args: Array<String>) {
parser.parse(args)
}
fun subcommandsInit() {
parser.subcommands(addNewText, showStatistics, showTextList, removeTexts)
} }
} }
@ -415,87 +420,29 @@ class CommandCenter(private val textData: TextData) {
* CommandCenter. If ReturnException was thrown, it is caught here, * CommandCenter. If ReturnException was thrown, it is caught here,
* last iteration breaks and new one is called. * last iteration breaks and new one is called.
*/ */
fun workCycle(commandCenter: CommandCenter) { fun readAndCallParsing(commandCenter: CommandCenter, mainArgs: Array<String>) {
while (true) { if (mainArgs.isNotEmpty()) commandCenter.parseArgsAndExecute(mainArgs)
else {
val args = readln().split(WHITESPACES).toTypedArray()
try { try {
(commandCenter.readCommandFromConsole())() commandCenter.parseArgsAndExecute(args)
} catch (e: ReturnException) { } catch (e: ReturnException) {
println("You have been returned in main menu.") println("message")
continue exit()
} }
} }
} }
/** Function reads from console input, check if it isn't available, repeat the request from the console
* in this case and convert input in type T if possible, else throws an exception and repeat the
* request.
*
* @param availableInputs list of available values, the choice among which
* is requested from the console at some stage of the program.
* @param unavailableInputs list of unavailable values.
* @return a pair of function, which could be called after the request from this function to return
* if user want it or continue and value that the user has selected from the list of available.
*/
fun requestInput(
availableInputs: List<Any>? = null,
unavailableInputs: List<Any>? = null
): Pair<InputOutcomeCommand, String?> {
val regexAvailableInputs = availableInputs?.joinToString("|")?.toRegex() ?: Regex(".*")
val regexUnavailableInputs = unavailableInputs?.joinToString("|")?.toRegex() ?: Regex("")
val returnRegex = Regex("return")
while (true) {
val input = readln().trim()
return when {
input.matches(regexAvailableInputs) && !input.matches(regexUnavailableInputs) ->
ContinueCommand() to input
input.matches(returnRegex) -> ReturnCommand() to null
else -> {
println("There isn't this elem in list of available inputs. Try to repeat or enter return to exit in main menu: ")
continue
}
}
}
}
/** Interface used to existing commands objects, whose exe value
* stores function which calls after requesting from console in
* requestInput function.
*/
interface InputOutcomeCommand {
/** Function returned in a Main.requestInput method for returned to
* main menu of application or continuation of the process.
*/
val exe: () -> Unit
}
/** If called exe of this object, program continue executing
* without changes.
*/
class ContinueCommand : InputOutcomeCommand {
override val exe: () -> Unit = {}
}
/** If called exe of this object, a custom ReturnException is thrown
* and process of executing some called command interrupted. Exception
* catches in mainCycle and program command request is repeated.
*/
class ReturnCommand : InputOutcomeCommand {
override val exe: () -> Unit = throw ReturnException()
}
/** Custom exception being thrown if user want to return to the menu. */ /** Custom exception being thrown if user want to return to the menu. */
class ReturnException : Exception() class ReturnException(message: String = "") : Exception(message)
fun main() { fun main(args: Array<String>) {
val textData = TextData() val textData = TextData()
val commandCenter = CommandCenter(textData) val commandCenter = CommandCenter(textData)
commandCenter.subcommandsInit()
workCycle(commandCenter) readAndCallParsing(commandCenter, args)
} }