2023-03-03 13:21:16 +00:00
|
|
|
/**
|
|
|
|
* Core logging services
|
|
|
|
*/
|
2021-12-23 13:16:31 +00:00
|
|
|
module dlog.core;
|
2021-12-23 13:14:51 +00:00
|
|
|
|
2021-12-23 10:14:36 +00:00
|
|
|
import std.conv : to;
|
2023-01-07 19:10:15 +00:00
|
|
|
import std.range : join;
|
2023-02-27 17:07:09 +00:00
|
|
|
import dlog.transform : MessageTransform;
|
2021-12-23 10:14:36 +00:00
|
|
|
import dlog.defaults;
|
2023-03-02 09:09:28 +00:00
|
|
|
import dlog.context : Context, CompilationInfo, Level;
|
2023-03-02 08:55:55 +00:00
|
|
|
import dlog.utilities : flatten;
|
2021-12-23 10:14:36 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Logger
|
|
|
|
*
|
|
|
|
* Represents a logger instance
|
|
|
|
*/
|
|
|
|
public class Logger
|
|
|
|
{
|
|
|
|
/* Starting transformation */
|
|
|
|
private MessageTransform messageTransform;
|
|
|
|
|
2023-01-07 19:10:15 +00:00
|
|
|
/* The multiple argument joiner */
|
2023-03-02 09:39:20 +00:00
|
|
|
protected string multiArgJoiner;
|
2023-01-07 19:10:15 +00:00
|
|
|
|
2023-03-02 09:09:28 +00:00
|
|
|
/**
|
|
|
|
* Constructs a new Logger with the default
|
|
|
|
* MessageTransform
|
|
|
|
*
|
|
|
|
* Params:
|
|
|
|
* multiArgJoiner = optional joiner for segmented prints (default is " ")
|
|
|
|
*/
|
2023-01-07 19:10:15 +00:00
|
|
|
this(string multiArgJoiner = " ")
|
2021-12-23 10:14:36 +00:00
|
|
|
{
|
2023-01-07 19:10:15 +00:00
|
|
|
this(new DefaultTransform(), multiArgJoiner);
|
2021-12-23 10:14:36 +00:00
|
|
|
}
|
|
|
|
|
2023-03-02 09:09:28 +00:00
|
|
|
/**
|
|
|
|
* Constructs a new Logger with the provided
|
|
|
|
* custom message transform
|
|
|
|
* Params:
|
|
|
|
* messageTransform = the message transform to use
|
|
|
|
* multiArgJoiner = optional joiner for segmented prints (default is " ")
|
|
|
|
*/
|
2023-01-07 19:10:15 +00:00
|
|
|
this(MessageTransform messageTransform, string multiArgJoiner = " ")
|
2021-12-23 10:14:36 +00:00
|
|
|
{
|
|
|
|
this.messageTransform = messageTransform;
|
2023-01-07 19:10:15 +00:00
|
|
|
this.multiArgJoiner = multiArgJoiner;
|
2021-12-23 10:14:36 +00:00
|
|
|
}
|
|
|
|
|
2023-03-02 08:53:08 +00:00
|
|
|
/**
|
|
|
|
* Given an arbitrary amount of arguments, convert each to a string
|
|
|
|
* and return it as an array joined by the joiner
|
|
|
|
*
|
|
|
|
* Params:
|
|
|
|
* segments = alias sequence
|
|
|
|
* Returns: a string of the argumnets
|
|
|
|
*/
|
|
|
|
public string args(TextType...)(TextType segments)
|
|
|
|
{
|
|
|
|
/* The flattened components */
|
|
|
|
string[] components = flatten(segments);
|
|
|
|
|
|
|
|
/* Join all `components` into a single string */
|
|
|
|
string joined = join(components, multiArgJoiner);
|
|
|
|
|
|
|
|
return joined;
|
|
|
|
}
|
|
|
|
|
2023-03-02 08:40:32 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Logs the given string using the default context
|
|
|
|
*
|
|
|
|
* Params:
|
|
|
|
* text = the string to log
|
|
|
|
* __FILE_FULL_PATH__ = compile time usage file
|
|
|
|
* __FILE__ = compile time usage file (relative)
|
|
|
|
* __LINE__ = compile time usage line number
|
|
|
|
* __MODULE__ = compile time usage module
|
|
|
|
* __FUNCTION__ = compile time usage function
|
|
|
|
* __PRETTY_FUNCTION__ = compile time usage function (pretty)
|
|
|
|
*/
|
2023-03-02 08:58:42 +00:00
|
|
|
public final void log(string text, string c1 = __FILE_FULL_PATH__,
|
2023-03-01 14:30:49 +00:00
|
|
|
string c2 = __FILE__, ulong c3 = __LINE__,
|
|
|
|
string c4 = __MODULE__, string c5 = __FUNCTION__,
|
|
|
|
string c6 = __PRETTY_FUNCTION__)
|
|
|
|
{
|
|
|
|
/* Use the default context `Context` */
|
|
|
|
Context defaultContext = new Context();
|
2023-03-01 07:15:10 +00:00
|
|
|
|
2023-03-01 14:30:49 +00:00
|
|
|
/* Build up the line information */
|
|
|
|
CompilationInfo compilationInfo = CompilationInfo(c1, c2, c3, c4, c5, c6);
|
2023-03-01 07:15:10 +00:00
|
|
|
|
2023-03-01 14:30:49 +00:00
|
|
|
/* Set the line information in the context */
|
|
|
|
defaultContext.setLineInfo(compilationInfo);
|
2023-03-01 07:15:10 +00:00
|
|
|
|
2023-03-01 14:30:49 +00:00
|
|
|
/* Call the log */
|
2023-03-02 08:58:42 +00:00
|
|
|
logc(defaultContext, text, c1, c2, c3, c4, c5, c6);
|
2023-03-01 07:15:10 +00:00
|
|
|
}
|
|
|
|
|
2023-03-02 08:40:32 +00:00
|
|
|
/**
|
|
|
|
* Logs using the default context an arbitrary amount of arguments
|
|
|
|
*
|
|
|
|
* Params:
|
|
|
|
* segments = the arbitrary argumnets (alias sequence)
|
|
|
|
* __FILE_FULL_PATH__ = compile time usage file
|
|
|
|
* __FILE__ = compile time usage file (relative)
|
|
|
|
* __LINE__ = compile time usage line number
|
|
|
|
* __MODULE__ = compile time usage module
|
|
|
|
* __FUNCTION__ = compile time usage function
|
|
|
|
* __PRETTY_FUNCTION__ = compile time usage function (pretty)
|
|
|
|
*/
|
2023-03-02 08:58:42 +00:00
|
|
|
public final void log(TextType...)(TextType segments, string c1 = __FILE_FULL_PATH__,
|
2023-03-02 08:40:32 +00:00
|
|
|
string c2 = __FILE__, ulong c3 = __LINE__,
|
|
|
|
string c4 = __MODULE__, string c5 = __FUNCTION__,
|
|
|
|
string c6 = __PRETTY_FUNCTION__)
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* Grab at compile-time all arguments and generate runtime code to add them to `components`
|
|
|
|
*/
|
2023-03-02 08:53:08 +00:00
|
|
|
string[] components = flatten(segments);
|
2023-03-02 08:40:32 +00:00
|
|
|
|
|
|
|
/* Join all `components` into a single string */
|
|
|
|
string messageOut = join(components, multiArgJoiner);
|
|
|
|
|
|
|
|
/* Call the log (with text and default context) */
|
2023-03-02 08:58:42 +00:00
|
|
|
log(messageOut, c1, c2, c3, c4, c5, c6);
|
2023-03-02 08:40:32 +00:00
|
|
|
}
|
2023-03-01 14:30:49 +00:00
|
|
|
|
2023-03-02 08:40:32 +00:00
|
|
|
/**
|
|
|
|
* Logs the given string using the provided context
|
|
|
|
*
|
|
|
|
* Params:
|
|
|
|
* context = the custom context to use
|
|
|
|
* text = the string to log
|
|
|
|
* __FILE_FULL_PATH__ = compile time usage file
|
|
|
|
* __FILE__ = compile time usage file (relative)
|
|
|
|
* __LINE__ = compile time usage line number
|
|
|
|
* __MODULE__ = compile time usage module
|
|
|
|
* __FUNCTION__ = compile time usage function
|
|
|
|
* __PRETTY_FUNCTION__ = compile time usage function (pretty)
|
|
|
|
*/
|
2023-03-02 08:58:42 +00:00
|
|
|
public final void logc(Context context, string text, string c1 = __FILE_FULL_PATH__,
|
2021-12-23 10:14:36 +00:00
|
|
|
string c2 = __FILE__, ulong c3 = __LINE__,
|
|
|
|
string c4 = __MODULE__, string c5 = __FUNCTION__,
|
2023-03-01 08:09:18 +00:00
|
|
|
string c6 = __PRETTY_FUNCTION__)
|
2021-12-23 10:14:36 +00:00
|
|
|
{
|
2023-03-01 14:30:49 +00:00
|
|
|
/* Build up the line information */
|
|
|
|
CompilationInfo compilationInfo = CompilationInfo(c1, c2, c3, c4, c5, c6);
|
2023-01-07 19:10:15 +00:00
|
|
|
|
2023-03-01 14:30:49 +00:00
|
|
|
/* Set the line information in the context */
|
|
|
|
context.setLineInfo(compilationInfo);
|
|
|
|
|
2021-12-23 10:14:36 +00:00
|
|
|
/* Apply the transformation on the message */
|
2023-03-02 08:40:32 +00:00
|
|
|
string transformedMesage = messageTransform.execute(text, context);
|
2021-12-23 10:14:36 +00:00
|
|
|
|
|
|
|
/* Call the underlying logger implementation */
|
|
|
|
logImpl(transformedMesage);
|
|
|
|
}
|
|
|
|
|
2023-03-02 09:09:28 +00:00
|
|
|
/**
|
|
|
|
* Logs using the default context an arbitrary amount of arguments
|
|
|
|
* specifically setting the context's level to ERROR
|
|
|
|
*
|
|
|
|
* Params:
|
|
|
|
* segments = the arbitrary argumnets (alias sequence)
|
|
|
|
* __FILE_FULL_PATH__ = compile time usage file
|
|
|
|
* __FILE__ = compile time usage file (relative)
|
|
|
|
* __LINE__ = compile time usage line number
|
|
|
|
* __MODULE__ = compile time usage module
|
|
|
|
* __FUNCTION__ = compile time usage function
|
|
|
|
* __PRETTY_FUNCTION__ = compile time usage function (pretty)
|
|
|
|
*/
|
2023-03-02 09:33:25 +00:00
|
|
|
public void error(TextType...)(TextType segments,
|
2023-03-02 09:09:28 +00:00
|
|
|
string c1 = __FILE_FULL_PATH__,
|
|
|
|
string c2 = __FILE__, ulong c3 = __LINE__,
|
|
|
|
string c4 = __MODULE__, string c5 = __FUNCTION__,
|
|
|
|
string c6 = __PRETTY_FUNCTION__)
|
|
|
|
{
|
|
|
|
/* Use the default context `Context` */
|
|
|
|
Context defaultContext = new Context();
|
|
|
|
|
|
|
|
/* Build up the line information */
|
|
|
|
CompilationInfo compilationInfo = CompilationInfo(c1, c2, c3, c4, c5, c6);
|
|
|
|
|
|
|
|
/* Set the line information in the context */
|
|
|
|
defaultContext.setLineInfo(compilationInfo);
|
|
|
|
|
2023-03-02 09:11:57 +00:00
|
|
|
/* Set the level to ERROR */
|
2023-03-02 09:09:28 +00:00
|
|
|
defaultContext.setLevel(Level.ERROR);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Grab at compile-time all arguments and generate runtime code to add them to `components`
|
|
|
|
*/
|
|
|
|
string[] components = flatten(segments);
|
|
|
|
|
|
|
|
/* Join all `components` into a single string */
|
|
|
|
string messageOut = join(components, multiArgJoiner);
|
|
|
|
|
2023-03-02 09:11:57 +00:00
|
|
|
/* Call the log */
|
|
|
|
logc(defaultContext, messageOut, c1, c2, c3, c4, c5, c6);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Logs using the default context an arbitrary amount of arguments
|
|
|
|
* specifically setting the context's level to INFO
|
|
|
|
*
|
|
|
|
* Params:
|
|
|
|
* segments = the arbitrary argumnets (alias sequence)
|
|
|
|
* __FILE_FULL_PATH__ = compile time usage file
|
|
|
|
* __FILE__ = compile time usage file (relative)
|
|
|
|
* __LINE__ = compile time usage line number
|
|
|
|
* __MODULE__ = compile time usage module
|
|
|
|
* __FUNCTION__ = compile time usage function
|
|
|
|
* __PRETTY_FUNCTION__ = compile time usage function (pretty)
|
|
|
|
*/
|
2023-03-02 09:33:25 +00:00
|
|
|
public void info(TextType...)(TextType segments,
|
2023-03-02 09:11:57 +00:00
|
|
|
string c1 = __FILE_FULL_PATH__,
|
|
|
|
string c2 = __FILE__, ulong c3 = __LINE__,
|
|
|
|
string c4 = __MODULE__, string c5 = __FUNCTION__,
|
|
|
|
string c6 = __PRETTY_FUNCTION__)
|
|
|
|
{
|
|
|
|
/* Use the default context `Context` */
|
|
|
|
Context defaultContext = new Context();
|
|
|
|
|
|
|
|
/* Build up the line information */
|
|
|
|
CompilationInfo compilationInfo = CompilationInfo(c1, c2, c3, c4, c5, c6);
|
|
|
|
|
|
|
|
/* Set the line information in the context */
|
|
|
|
defaultContext.setLineInfo(compilationInfo);
|
2023-03-02 09:09:28 +00:00
|
|
|
|
2023-03-02 09:11:57 +00:00
|
|
|
/* Set the level to INFO */
|
|
|
|
defaultContext.setLevel(Level.INFO);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Grab at compile-time all arguments and generate runtime code to add them to `components`
|
|
|
|
*/
|
|
|
|
string[] components = flatten(segments);
|
|
|
|
|
|
|
|
/* Join all `components` into a single string */
|
|
|
|
string messageOut = join(components, multiArgJoiner);
|
|
|
|
|
|
|
|
/* Call the log */
|
|
|
|
logc(defaultContext, messageOut, c1, c2, c3, c4, c5, c6);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Logs using the default context an arbitrary amount of arguments
|
|
|
|
* specifically setting the context's level to WARN
|
|
|
|
*
|
|
|
|
* Params:
|
|
|
|
* segments = the arbitrary argumnets (alias sequence)
|
|
|
|
* __FILE_FULL_PATH__ = compile time usage file
|
|
|
|
* __FILE__ = compile time usage file (relative)
|
|
|
|
* __LINE__ = compile time usage line number
|
|
|
|
* __MODULE__ = compile time usage module
|
|
|
|
* __FUNCTION__ = compile time usage function
|
|
|
|
* __PRETTY_FUNCTION__ = compile time usage function (pretty)
|
|
|
|
*/
|
2023-03-02 09:33:25 +00:00
|
|
|
public void warn(TextType...)(TextType segments,
|
2023-03-02 09:11:57 +00:00
|
|
|
string c1 = __FILE_FULL_PATH__,
|
|
|
|
string c2 = __FILE__, ulong c3 = __LINE__,
|
|
|
|
string c4 = __MODULE__, string c5 = __FUNCTION__,
|
|
|
|
string c6 = __PRETTY_FUNCTION__)
|
|
|
|
{
|
|
|
|
/* Use the default context `Context` */
|
|
|
|
Context defaultContext = new Context();
|
|
|
|
|
|
|
|
/* Build up the line information */
|
|
|
|
CompilationInfo compilationInfo = CompilationInfo(c1, c2, c3, c4, c5, c6);
|
|
|
|
|
|
|
|
/* Set the line information in the context */
|
|
|
|
defaultContext.setLineInfo(compilationInfo);
|
|
|
|
|
|
|
|
/* Set the level to WARN */
|
|
|
|
defaultContext.setLevel(Level.WARN);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Grab at compile-time all arguments and generate runtime code to add them to `components`
|
|
|
|
*/
|
|
|
|
string[] components = flatten(segments);
|
|
|
|
|
|
|
|
/* Join all `components` into a single string */
|
|
|
|
string messageOut = join(components, multiArgJoiner);
|
|
|
|
|
|
|
|
/* Call the log */
|
|
|
|
logc(defaultContext, messageOut, c1, c2, c3, c4, c5, c6);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Logs using the default context an arbitrary amount of arguments
|
|
|
|
* specifically setting the context's level to DEBUG
|
|
|
|
*
|
|
|
|
* Params:
|
|
|
|
* segments = the arbitrary argumnets (alias sequence)
|
|
|
|
* __FILE_FULL_PATH__ = compile time usage file
|
|
|
|
* __FILE__ = compile time usage file (relative)
|
|
|
|
* __LINE__ = compile time usage line number
|
|
|
|
* __MODULE__ = compile time usage module
|
|
|
|
* __FUNCTION__ = compile time usage function
|
|
|
|
* __PRETTY_FUNCTION__ = compile time usage function (pretty)
|
|
|
|
*/
|
2023-03-02 09:33:25 +00:00
|
|
|
public void debug_(TextType...)(TextType segments,
|
2023-03-02 09:11:57 +00:00
|
|
|
string c1 = __FILE_FULL_PATH__,
|
|
|
|
string c2 = __FILE__, ulong c3 = __LINE__,
|
|
|
|
string c4 = __MODULE__, string c5 = __FUNCTION__,
|
|
|
|
string c6 = __PRETTY_FUNCTION__)
|
|
|
|
{
|
|
|
|
/* Use the default context `Context` */
|
|
|
|
Context defaultContext = new Context();
|
|
|
|
|
|
|
|
/* Build up the line information */
|
|
|
|
CompilationInfo compilationInfo = CompilationInfo(c1, c2, c3, c4, c5, c6);
|
|
|
|
|
|
|
|
/* Set the line information in the context */
|
|
|
|
defaultContext.setLineInfo(compilationInfo);
|
|
|
|
|
|
|
|
/* Set the level to DEBUG */
|
|
|
|
defaultContext.setLevel(Level.DEBUG);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Grab at compile-time all arguments and generate runtime code to add them to `components`
|
|
|
|
*/
|
|
|
|
string[] components = flatten(segments);
|
|
|
|
|
|
|
|
/* Join all `components` into a single string */
|
|
|
|
string messageOut = join(components, multiArgJoiner);
|
2023-03-02 09:09:28 +00:00
|
|
|
|
|
|
|
/* Call the log */
|
|
|
|
logc(defaultContext, messageOut, c1, c2, c3, c4, c5, c6);
|
|
|
|
}
|
2023-03-02 09:12:35 +00:00
|
|
|
|
2023-03-03 13:23:12 +00:00
|
|
|
/**
|
|
|
|
* Alias for debug_
|
|
|
|
*/
|
2023-03-02 09:12:35 +00:00
|
|
|
public alias dbg = debug_;
|
2023-03-02 08:40:32 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Logging implementation, this is where the final
|
|
|
|
* transformed text will be transferred to and finally
|
|
|
|
* logged
|
|
|
|
*
|
|
|
|
* Params:
|
|
|
|
* message = the message to log
|
|
|
|
*/
|
2021-12-23 10:14:36 +00:00
|
|
|
protected abstract void logImpl(string message);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-03-01 07:15:10 +00:00
|
|
|
version(unittest)
|
|
|
|
{
|
|
|
|
import std.meta : AliasSeq;
|
|
|
|
import std.stdio : writeln;
|
|
|
|
}
|
2021-12-23 10:14:36 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Tests the DefaultLogger
|
|
|
|
*/
|
|
|
|
unittest
|
|
|
|
{
|
|
|
|
Logger logger = new DefaultLogger();
|
|
|
|
|
2023-01-07 19:17:39 +00:00
|
|
|
alias testParameters = AliasSeq!("This is a log message", 1.1, true, [1,2,3], 'f', logger);
|
2023-01-07 19:10:15 +00:00
|
|
|
|
2023-01-07 19:17:39 +00:00
|
|
|
|
|
|
|
// Test various types one-by-one
|
|
|
|
static foreach(testParameter; testParameters)
|
|
|
|
{
|
|
|
|
logger.log(testParameter);
|
|
|
|
}
|
2023-01-07 19:10:15 +00:00
|
|
|
|
2023-01-07 19:17:39 +00:00
|
|
|
// Test various parameters (of various types) all at once
|
|
|
|
logger.log(testParameters);
|
2023-01-07 19:10:15 +00:00
|
|
|
|
2023-01-07 19:17:39 +00:00
|
|
|
// Same as above but with a custom joiner set
|
2023-01-07 19:10:15 +00:00
|
|
|
logger = new DefaultLogger("(-)");
|
2023-01-07 19:17:39 +00:00
|
|
|
logger.log(testParameters);
|
2023-03-01 07:15:10 +00:00
|
|
|
|
|
|
|
writeln();
|
|
|
|
}
|
|
|
|
|
2023-03-01 08:09:18 +00:00
|
|
|
/**
|
2023-03-02 08:53:08 +00:00
|
|
|
* Printing out some mixed data-types, also using a DEFAULT context
|
2023-03-01 14:30:49 +00:00
|
|
|
*/
|
|
|
|
unittest
|
|
|
|
{
|
|
|
|
Logger logger = new DefaultLogger();
|
|
|
|
|
2023-03-02 08:53:08 +00:00
|
|
|
// Create a default logger with the default joiner
|
2023-03-01 14:30:49 +00:00
|
|
|
logger = new DefaultLogger();
|
|
|
|
logger.log(["a", "b", "c", "d"], [1, 2], true);
|
|
|
|
|
|
|
|
writeln();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-03-02 08:53:08 +00:00
|
|
|
* Printing out some mixed data-types, also using a CUSTOM context
|
2023-03-01 14:30:49 +00:00
|
|
|
*/
|
|
|
|
unittest
|
|
|
|
{
|
|
|
|
Logger logger = new DefaultLogger();
|
|
|
|
|
2023-03-02 08:53:08 +00:00
|
|
|
// Create a default logger with the default joiner
|
|
|
|
logger = new DefaultLogger();
|
2023-03-01 14:30:49 +00:00
|
|
|
|
2023-03-02 09:09:28 +00:00
|
|
|
// Create a custom context
|
2023-03-01 14:30:49 +00:00
|
|
|
Context customContext = new Context();
|
|
|
|
|
|
|
|
// Log with the custom context
|
2023-03-02 08:53:08 +00:00
|
|
|
logger.logc(customContext, logger.args(["an", "array"], 1, "hello", true));
|
2023-03-01 14:30:49 +00:00
|
|
|
|
2023-03-02 09:09:28 +00:00
|
|
|
writeln();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Printing out some mixed data-types, also using a DEFAULT context
|
|
|
|
* but also testing out the `error()`, `warn()`, `info()` and `debug()`
|
|
|
|
*/
|
|
|
|
unittest
|
|
|
|
{
|
|
|
|
Logger logger = new DefaultLogger();
|
|
|
|
|
|
|
|
// Create a default logger with the default joiner
|
|
|
|
logger = new DefaultLogger();
|
|
|
|
|
|
|
|
// Test out `error()`
|
|
|
|
logger.error(["woah", "LEVELS!"], 69.420);
|
|
|
|
|
|
|
|
// Test out `info()`
|
|
|
|
logger.info(["woah", "LEVELS!"], 69.420);
|
|
|
|
|
|
|
|
// Test out `warn()`
|
|
|
|
logger.warn(["woah", "LEVELS!"], 69.420);
|
|
|
|
|
|
|
|
// Test out `debug_()`
|
|
|
|
logger.debug_(["woah", "LEVELS!"], 69.420);
|
|
|
|
|
2023-03-01 14:30:49 +00:00
|
|
|
writeln();
|
2023-01-07 19:10:15 +00:00
|
|
|
}
|