Please consider the following records
I'm trying to flatten and group the data by Robot Name then by date + Left Factory time then group the address(es) for that date and time. Notice that some of the Left Factory times are identical.
I wrote the code below and it works. It gives me the output that I want. I was a Perl developer so what you see below is from that mentality. I'm sure there is a better way of doing it in LINQ. A little help please.
static void Main(string[] args)
{
if (args.Length < 0){
Console.WriteLine("Input file name is required");
return;
}
List<string> rawlst = File.ReadAllLines(args[0]).ToList<string>();
Dictionary<string, Dictionary<DateTime, List<string>>> dicDriver = new Dictionary<string, Dictionary<DateTime, List<string>>>();
foreach (string ln in rawlst)
{
try
{
List<string> parts = new List<string>();
parts = ln.Split(',').ToList<string>();
string[] dtparts = parts[1].Split('/');
string[] dttime = parts[15].Split(':');
DateTime dtrow = new DateTime(
int.Parse(dtparts[2]), int.Parse(dtparts[0]), int.Parse(dtparts[1]),
int.Parse(dttime[0]), int.Parse(dttime[1]), int.Parse(dttime[2]));
string rowAddress = parts[7] + " " + parts[9] + " " + parts[10] + " " + parts[11];
if (!dicDriver.Keys.Contains(parts[3]))
{
Dictionary<DateTime, List<string>> thisRec = new Dictionary<DateTime, List<string>>();
thisRec.Add(dtrow, new List<string>() { rowAddress });
dicDriver.Add(parts[3], thisRec);
}
else
{
Dictionary<DateTime, List<string>> thisDriver = new Dictionary<DateTime, List<string>>();
thisDriver = dicDriver[parts[3]];
if (!thisDriver.Keys.Contains(dtrow))
{
dicDriver[parts[3]].Add(dtrow, new List<string>() { rowAddress });
}
else
{
dicDriver[parts[3]][dtrow].Add(rowAddress);
}
}
}
catch (Exception e)
{
Console.WriteLine("ERROR:" + ln);
}
}
//output
string filename = DateTime.Now.Ticks.ToString() + ".out";
foreach (var name in dicDriver.Keys)
{
foreach (var dd in dicDriver[name])
{
Console.Write(name + "," + dd.Key + ",");
File.AppendAllText(filename, name + "," + dd.Key + Environment.NewLine);
foreach (var addr in dd.Value)
{
Console.Write("\t\t" + addr + Environment.NewLine);
File.AppendAllText(filename, "\t" + addr + Environment.NewLine);
}
}
Console.Write(Environment.NewLine);
File.AppendAllText(filename, Environment.NewLine);
}
Console.ReadLine();
}
You should separate your concerns: separate your input from the processing and from the output.
For example: suppose you would have to read your input from a database instead from a CSV file? Would that seriously change the way your process your fetched data? In your design, fetching the data is mixed with processing: although you know that the data that you want to process contains something like FactoryProcesses, you decide to present eache FactoryProcess as a string. A FactoryProcess is not a string. It describes how and when and who processed something in your factory. That is not a string, is it? However, it might be represented internally as a string, but the outside world should not know this. This way, if you change your FactoryProcess from being read by a CSV-file, to something provided by a database, the users of your FactoryProcess won't see any difference.
Separation of concerns makes your code easier to understand, easier to test, easier to change, and better to re-use.
So let's separate!
IEnumerable<FactoryProcess> ReadFactoryProcesses(string fileName)
{
// TODO: check fileName not null, file exists
using (var fileReader = new StreamReader(fileName))
{
// read the file Line by Line and split each line into one FactoryProcess object
string line = fileReader.ReadLine();
while (line != null)
{
// one line read, convert to FactoryProcess and yield return:
FactoryProcess factoryProcess = this.ToFactoryProcess(line);
yield return factoryProcess;
// read next line:
line = fileReader.ReadLine();
}
}
}
I'll leave the conversion of a read line into a FactoryProcess up to you. Tip: if the items in your lines are separated by a comma, or something similar, consider using Nuget Package CSVHelper. It makes if easier to convert a file into a sequence of FactoryProcesses.
I want to group the data by Robot Name then by date + Left Factory time then group the address(es) for that date and time.
First of all: make sure that the FactoryProcess class has the properties you actually need. Separate this representation from what it is in a file. Apparently you want to tread date + left factory as one item that represents the Date and Time that it left the factory. So Let's create a DateTime property for this.
class FactoryProcess
{
public int Id {get; set}
public int PartNo {get; set;}
public string RobotName {get; set;} // or if desired: use a unique RobotId
...
// DateTimes: ArrivalTime, OutOfFactoryTime, LeftFactoryTime
public DateTime ArrivalTime {get; set;}
public DateTime OutOfFactoryTime {get; set;}
public DateTime LeftFactoryTime {get; set;}
}
The reason that I put Date and Time into one DateTime, is because it will solve problems if an item arrives on 23:55 and leaves on 00:05 next day.
A procedure that converts a read CSV-line to a FactoryProcess should interpret your dates and times as strings and convert to FactoryProcess. You can create a constrcuctor for this, or a special Factory class
public FactoryProcess InterpretReadLine(string line)
{
// TODO: separate the parts, such that you've got the strings dateTxt, arrivalTimeTxt, ...
DateTime date = DateTime.Parse(dateTxt);
TimeSpan arrivalTime = TimeSpan.Parse(arrivalTimeTxt);
TimeSpan outOfFactoryTime = TimeSpan.Parse(outOfFactoryTimeTxt);
TimeSpan leftFactoryTime = TimeSpan.Parse(leftFactoryTimeTxt);
return new FactoryProces
{
Id = ...
PartNo = ..
RobotName = ...
// The DateTimes:
ArrivalTime = date + arrivalTime,
OutOfFactoryTime = date + outOfFactoryTime,
LeftFactoryTime = date + leftFactoryTime,
};
}
Now that you've created a proper method to convert your CSV-file into a sequence of FactoryProcesses, let's process them
I want to group the data by Robot Name then by date + Left Factory time then group the address(es) for that date and time.
var result = fetchedFactoryProcesses.GroupBy(
// parameter KeySelector: make groups of FactoryProcesses with same RobotName:
factoryProcess => factoryProcess.RobotName,
// parameter ResultSelector: from every group of FactoryProcesses with this RobotName
// make one new Object:
(robotName, processesWithThisRobotName) => new
{
RobotName = robotName,
// Group all processes with this RobotName into groups with same LeftFactoryTime:
LeftFactory = processesWithThisRobotName.GroupBy(
// KeySelector: make groups with same LeftFactoryTime
process => process.LeftFactoryTime,
// ResultSelector: from each group of factory processes with the same LeftFactoryTime
(leftFactoryTime, processesWithThisLeftFactoryTime) => new
{
LeftFactoryTime = leftFactoryTime,
FactoryProcesses = processesWithThisLeftFactoryTime,
// or even better: select only the properties you actually plan to use
FactoryProcesses = processesWithThisLeftFactoryTime.Select(process => new
{
Id = process.Id,
PartNo = process.PartNo,
...
// not needed: you know the value, because it is in this group
// RobotName = process.RobotName,
// LeftFactoryTime = process.LeftFactoryTime,
}),
})
});
For completeness: grouping your code together:
void ProcessData(string fileName)
{
var fetchedFactoryProcesses = ReadFactoryProcess(fileName); // fetch the data
var groups = fetchFactoryProcesses.ToGroups(); // put into groups
this.Display(groups); // output result;
}
Because I separated the input from the conversion of strings to FactoryProcesses, and separated this conversion from the grouping, it will be easy to test the classes separately:
your CSV-reader should return any file that is divided into lines, even if it does not contain FactoryProcesses
your conversion from read line to FactoryProcess should convert any string that is in the proper format, whether it is read from a file or gathered any other way
your grouping should group any sequence of FactoryProcesses, whether they come from a CSV-file or from a database or List<FactoryProcess>, which is very convenient, because in your tests it is way easier to create a test list, than a test CSV-file.
If in future you decide to change the source of your sequence of FactoryProcesses, for instance it comes from a database instead of a CSV-file, your grouping won't change. Or if you decide to support entering and leaving factories on different dates (multiple date values) only the conversion changes. If you decide to display the results in a tree-like fashion, or decide to write the groups in a database, your reading, conversion, grouping, etc won't change: what a high degree or re-usability!
Separating your concerns made it much easier to understand how to solve your grouping problem, without the hassle of splitting your read lines and converting Date + LeftFactory into one value.
Related
Does anyone know how to get a workflow malfunction error message using the java pe api? I am running the QueueSample java code provided by IBM and it is not clear to me how to do this. Any help would be appreciated!
I found the malfunction error message for my workflow in the VWParticipantHistory.getLogFields() array. I modified the example code from the Developing Applications with IBM FileNet P8 APIs redbook:
// Create session object and log onto Process Engine
...
// Get the specific work item
...
// Get VWProcess object from work object
VWProcess process = stepElement.fetchProcess();
// Get workflow definitions from the VWProcess
VWWorkflowDefinition workflowDefinition =
process.fetchWorkflowDefinition(false);
// Get maps for each workflow definition
VWMapDefinition[] workflowMaps = workflowDefinition.getMaps();
// Iterate through each map in the workflow Definition
for (int i = 0; i < workflowMaps.length; i++) {
// Get map ID and map name for each map definition
int mapID = workflowMaps[i].getMapId();
String mapName = workflowMaps[i].getName();
// Get workflow history information for each map
VWWorkflowHistory workflowHistory = process.fetchWorkflowHistory(mapID);
String workflowOriginator = workflowHistory.getOriginator();
// Iterate through each item in the Workflow History
while (workflowHistory.hasNext()) {
// Get step history objects for each workflow history
VWStepHistory stepHistory = workflowHistory.next();
String stepName = stepHistory.getStepName();
System.out.println("step history name = " + stepName);
// Iterate through each item in the Step History
while (stepHistory.hasNext()) {
// Get step occurrence history
// objects for each step history object
VWStepOccurrenceHistory stepOccurenceHistory = stepHistory.next();
Date stepOcurrenceDateReceived = stepOccurenceHistory.getDateReceived();
Date stepOcurrenceDateCompleted = stepOccurenceHistory.getCompletionDate();
while (stepOccurenceHistory.hasNext()) {
// Get step work object information
// for each step occurrence
VWStepWorkObjectHistory stepWorkObjectHistory = stepOccurenceHistory.next();
stepWorkObjectHistory.resetFetch();
// Get participant information for each work object
while (stepWorkObjectHistory.hasNext()) {
VWParticipantHistory participantHistory = stepWorkObjectHistory.next();
String opName = participantHistory.getOperationName();
System.out.println("operation name = " + opName);
Date participantDateReceived = participantHistory.getDateReceived();
String participantComments = participantHistory.getComments();
String participantUser = participantHistory.getUserName();
String participantName = participantHistory.getParticipantName();
VWDataField[] logFields = participantHistory.getLogFields();
System.out.println("** start get log fields **");
for (int index=0; index<logFields.length; index++){
VWDataField dataField = logFields[index];
String name = dataField.getName();
String val = dataField.getStringValue();
System.out.println("name = " + name + " , value = " + val);
}
System.out.println("** end get log fields **");
} // while stepWorkObjectHistory
} // while stepOccurenceHistory
} // while stepHistory
} // while workflowHistory
} // for workflow maps
I'm fairly new to Oracle but I have used the Bulk insert on a couple other applications. Most seem to go faster using it but I've had a couple where it slows down the application. This is my second one where it slowed it down significantly so I'm wondering if I have something setup incorrectly or maybe I need to set it up differently. In this case I have a console application that processed ~1,900 records. Inserting them individually it will take ~2.5 hours and when I switched over to the Bulk insert it jumped to 5 hours.
The article I based this off of is http://www.oracle.com/technetwork/issue-archive/2009/09-sep/o59odpnet-085168.html
Here is what I'm doing, I'm retrieving some records from the DB, do calculations, and then write the results out to a text file. After the calculations are done I have to write those results back to a different table in the DB so we can look back at what those calculations later on if needed.
When I make the calculation I add the results to a List. Once I'm done writing out the file I look at that List and if there are any records I do the bulk insert.
With the bulk insert I have a setting in the App.config to set the number of records I want to insert. In this case I'm using 250 records. I assumed it would be better to limit my in memory arrays to say 250 records versus the 1,900. I loop through that list to the count in the App.config and create an array for each column. Those arrays are then passed as parameters to Oracle.
App.config
<add key="UpdateBatchCount" value="250" />
Class
class EligibleHours
{
public string EmployeeID { get; set; }
public decimal Hours { get; set; }
public string HoursSource { get; set; }
}
Data Manager
public static void SaveEligibleHours(List<EligibleHours> listHours)
{
//set the number of records to update batch on from config file Subtract one because of 0 based index
int batchCount = int.Parse(ConfigurationManager.AppSettings["UpdateBatchCount"]);
//create the arrays to add values to
string[] arrEmployeeId = new string[batchCount];
decimal[] arrHours = new decimal[batchCount];
string[] arrHoursSource = new string[batchCount];
int i = 0;
foreach (var item in listHours)
{
//Create an array of employee numbers that will be used for a batch update.
//update after every X amount of records, update. Add 1 to i to compensated for 0 based indexing.
if (i + 1 <= batchCount)
{
arrEmployeeId[i] = item.EmployeeID;
arrHours[i] = item.Hours;
arrHoursSource[i] = item.HoursSource;
i++;
}
else
{
UpdateDbWithEligibleHours(arrEmployeeId, arrHours, arrHoursSource);
//reset counter and array
i = 0;
arrEmployeeId = new string[batchCount];
arrHours = new decimal[batchCount];
arrHoursSource = new string[batchCount];
}
}
//process last array
if (arrEmployeeId.Length > 0)
{
UpdateDbWithEligibleHours(arrEmployeeId, arrHours, arrHoursSource);
}
}
private static void UpdateDbWithEligibleHours(string[] arrEmployeeId, decimal[] arrHours, string[] arrHoursSource)
{
StringBuilder sbQuery = new StringBuilder();
sbQuery.Append("insert into ELIGIBLE_HOURS ");
sbQuery.Append("(EMP_ID, HOURS_SOURCE, TOT_ELIG_HRS, REPORT_DATE) ");
sbQuery.Append("values ");
sbQuery.Append("(:1, :2, :3, SYSDATE) ");
string connectionString = ConfigurationManager.ConnectionStrings["Server_Connection"].ToString();
using (OracleConnection dbConn = new OracleConnection(connectionString))
{
dbConn.Open();
//create Oracle parameters and pass arrays of data
OracleParameter p_employee_id = new OracleParameter();
p_employee_id.OracleDbType = OracleDbType.Char;
p_employee_id.Value = arrEmployeeId;
OracleParameter p_hoursSource = new OracleParameter();
p_hoursSource.OracleDbType = OracleDbType.Char;
p_hoursSource.Value = arrHoursSource;
OracleParameter p_hours = new OracleParameter();
p_hours.OracleDbType = OracleDbType.Decimal;
p_hours.Value = arrHours;
OracleCommand objCmd = dbConn.CreateCommand();
objCmd.CommandText = sbQuery.ToString();
objCmd.ArrayBindCount = arrEmployeeId.Length;
objCmd.Parameters.Add(p_employee_id);
objCmd.Parameters.Add(p_hoursSource);
objCmd.Parameters.Add(p_hours);
objCmd.ExecuteNonQuery();
}
}
i have a single textbox named Keywords.
User can enter multiple strings for search.
How this is possible in mvc3?
I am using nhibernate as ORM.
Can i create criteria for this?
Edited Scenario
I have partial view to search job based on following values:
Keywords(multiple strings), Industry(cascading dropdown with functional area )//working well ,FunctionalArea//working well
Loaction(multiple locations), Experience//working well
In Controller i am retrieving these values from form collection.
What datatype should i use for keywords and location (string or string[] )?
public ActionResult SearchResult(FormCollection formCollection)
{
IList<Jobs> JobsSearchResultList = new List<Jobs>();
//string[] keywords = null;
string location = null;
int? industry = 0;
int? functionaArea = 0;
int? experience = 0;
string keywords = null;
if (formCollection["txtKeyword"] != "")
{
keywords = formCollection["txtKeyword"];
}
//if (formCollection["txtKeyword"] != "")
//{
// keywordAry = formCollection["txtKeyword"].Split(' ');
// foreach (string keyword in keywordAry)
// {
// string value = keyword;
// }
//}
......retrieving other values from formcollection
....
//Now passing these values to Service method where i have criteria for job search
JobsSearchResultList = oEasyJobsService.GetJobsOnSearchExists(keywords,industry,functionaArea,location,experience);
return View(JobsSearchResultList);
}
In Services i have done like:
public IList<EASYJobs> GetJobsOnSearchExists(string keywords, int? industryId, int? functionalAreaId, string location, int? experience)
{
IList<JobLocation> locationlist = new List<JobLocation>();
IList<Jobs> JobsList = null;
var disjunction = Expression.Disjunction();
ICriteria query = session.CreateCriteria(typeof(Jobs), "EJobs");
if (keywords != null)
{
foreach (string keyword in keywords)
{
string pattern = String.Format("%{0}%", keyword);
disjunction
.Add(Restrictions.InsensitiveLike("Jobs.keywords", pattern,MatchMode.Anywhere))
.Add(Restrictions.InsensitiveLike("YJobs.PostTitle",pattern,MatchMode.Anywhere));
}
query.Add(disjunction)
.Add(Expression.Eq("EASYJobs.Industry.IndustryId", industryId))
.Add(Expression.Eq("Jobs.FunctionalArea.FunctionalAreaId", functionalAreaId))
.Add(Expression.Eq("Jobs.RequiredExperience", experience)));
}
else
{..
}
JobsList = criteria.List<Jobs>();
}
Problems i am facing are:
In controller if i use string[],then Split(',') does not split the string with specified separator.It passes string as it is to Service.
2.In services i am trying to replace string with %{0}% ,strings with spaces are replaced/concat() here with given delimeter.
But the problem here is It always return the whole job list means not giving the required output.
Pleas help ...
As long as you have a delimiter you can break the input into pieces on you should be able to create an or expression with the parts. You can use a disjunction to combine an arbitrary number of criteria using OR's.
var criteria = session.CreateCriteria<TestObject>();
Junction disjunction = Restrictions.Disjunction();
var input = "key words";
foreach (var keyword in input.Split(" "))
{
ICriterion criterion = Restrictions.Eq("PropertyName", keyword);
disjunction.Add(criterion);
}
criteria.Add(disjunction);
Multiple keywords with special characters or extra spaces are replaced with single space with Regex expressions.
And then keywords are separated with Split("").
Its working as required....
if (!string.IsNullOrEmpty(keywords))
{
keywords = keywords.Trim();
keywords = System.Text.RegularExpressions.Regex.Replace(keywords, #"[^0-9a-zA-Z\._\s]", " ");
keywords = System.Text.RegularExpressions.Regex.Replace(keywords, #"[\s]+", " ");
if (keywords.IndexOf(" ") > 0)
{
string[] arr = keywords.Split(" ".ToCharArray());
for (int i = 0; i < arr.Length; i++)
{
if (!string.IsNullOrEmpty(arr[i]))
{
criteria.Add(Restrictions.Disjunction()
.Add(Expression.Like("EASYJobs.keywords", arr[i], MatchMode.Anywhere)));
}
}
}
else
{
criteria.Add(Restrictions.Disjunction()
.Add(Expression.Like("EASYJobs.keywords", keywords, MatchMode.Anywhere)));
}
}
I have a sample CSV file as follows
1,A
2,B
3,C
Code:
var query = File.ReadAllLines("test.txt")
.Select(record => record.Split(','))
.Select(tokens => new { clearNum = tokens[0], MPID = tokens[1] });
foreach (var item in query)
{
Console.WriteLine("{0}, {1}", item.clearNum, item.MPID);
}
I am able to print the items.
I need to send the output of LINQ query to LIST
public class icSASList
{
public string ClearNum { get; set; }
public string MPID { get; set; }
}
List clearList = new List;
After considering the accepted answer, I'd like to suggest a solution that requires less object initializations. If the list is large, this will make a difference.
var query = File.ReadAllLines("test.txt")
.Select(record => record.Split(','))
.Select(tokens => new icSASList(){ ClearNum = tokens[0], MPID = tokens[1] });
var clearList = query.ToList();
Oh, yeah, using record.Split(',') is naive - it's normally allowed to have commas in " (quoted) fields, which will break your program. Better use something like http://www.filehelpers.com/.
I've not tried compiling it but I think you want something like this?
var clearList = query.Select(x=>new icSASList(){ClearNum = x.clearNum, MPID = x.MPID}).ToList();
I want to extract information of bullets present in word document.
I want something like this :
Suppose the text below, is in word document :
Steps to Start car :
Open door
Sit inside
Close the door
Insert key
etc.
Then I want my text file like below :
Steps to Start car :
<BULET> Open door </BULET>
<BULET> Sit inside </BULET>
<BULET> Close the door </BULET>
<BULET> Insert key </BULET>
<BULET> etc.</BULET>
I am using C# language to do this.
I can extract paragraphs from word document and directly write them in text file with some formatting information like whether text is bold or is in italics, etc. but dont know how to extract this bullet information.
Can anyone please tell me how to do this?
Thanks in advance
You can do it by reading each sentence. doc.Sentences is an array of Range object. So you can get same Range object from Paragraph.
foreach (Paragraph para in oDoc.Paragraphs)
{
string paraNumber = para.Range.ListFormat.ListLevelNumber.ToString();
string bulletStr = para.Range.ListFormat.ListString;
MessageBox.Show(paraNumber + "\t" + bulletStr + "\t" + para.Range.Text);
}
Into paraNumber you can get paragraph level and into buttetStr you can get bullet as string.
I am using this OpenXMLPower tool by Eric White. Its free and available at NUGet package. you can install it from Visual studio package manager.
He has provided a ready to use code snippet. This tool has saved me many hours. Below is the way I have customized code snippet to use for my requirement.
Infact you can use these methods as it in your project.
private static WordprocessingDocument _wordDocument;
private StringBuilder textItemSB = new StringBuilder();
private List<string> textItemList = new List<string>();
/// Open word document using office SDK and reads all contents from body of document
/// </summary>
/// <param name="filepath">path of file to be processed</param>
/// <returns>List of paragraphs with their text contents</returns>
private void GetDocumentBodyContents()
{
string modifiedString = string.Empty;
List<string> allList = new List<string>();
List<string> allListText = new List<string>();
try
{
_wordDocument = WordprocessingDocument.Open(wordFileStream, false);
//RevisionAccepter.AcceptRevisions(_wordDocument);
XElement root = _wordDocument.MainDocumentPart.GetXDocument().Root;
XElement body = root.LogicalChildrenContent().First();
OutputBlockLevelContent(_wordDocument, body);
}
catch (Exception ex)
{
logger.Error("ERROR in GetDocumentBodyContents:" + ex.Message.ToString());
}
}
// This is recursive method. At each iteration it tries to fetch listitem and Text item. Once you have these items in hand
// You can manipulate and create your own collection.
private void OutputBlockLevelContent(WordprocessingDocument wordDoc, XElement blockLevelContentContainer)
{
try
{
string listItem = string.Empty, itemText = string.Empty, numberText = string.Empty;
foreach (XElement blockLevelContentElement in
blockLevelContentContainer.LogicalChildrenContent())
{
if (blockLevelContentElement.Name == W.p)
{
listItem = ListItemRetriever.RetrieveListItem(wordDoc, blockLevelContentElement);
itemText = blockLevelContentElement
.LogicalChildrenContent(W.r)
.LogicalChildrenContent(W.t)
.Select(t => (string)t)
.StringConcatenate();
if (itemText.Trim().Length > 0)
{
if (null == listItem)
{
// Add html break tag
textItemSB.Append( itemText + "<br/>");
}
else
{
//if listItem == "" bullet character, replace it with equivalent html encoded character
textItemSB.Append(" " + (listItem == "" ? "•" : listItem) + " " + itemText + "<br/>");
}
}
else if (null != listItem)
{
//If bullet character is found, replace it with equivalent html encoded character
textItemSB.Append(listItem == "" ? " •" : listItem);
}
else
textItemSB.Append("<blank>");
continue;
}
// If element is not a paragraph, it must be a table.
foreach (var row in blockLevelContentElement.LogicalChildrenContent())
{
foreach (var cell in row.LogicalChildrenContent())
{
// Cells are a block-level content container, so can call this method recursively.
OutputBlockLevelContent(wordDoc, cell);
}
}
}
if (textItemSB.Length > 0)
{
textItemList.Add(textItemSB.ToString());
textItemSB.Clear();
}
}
catch (Exception ex)
{
.....
}
}
I got the answer.....
First I was converting doc on paragraph basis. But instead of that if we process doc file sentence by sentence basis, it is possible to determine whether that sentence contains bullet or any kind of shape or if that sentence is part of table. So once we get this information, then we can convert that sentence appropriately. If someone needs source code, I can share it.