I have implemented an algorithm that will generate unique names for files that will save on hard drive. I'm appending DateTime: Hours,Minutes,Second and Milliseconds but still it generates duplicate name of files because im uploading multiple files at a time.
What is the best solution to generate unique names for files to be stored on hard drive so no 2 files are same?
If readability doesn't matter, use GUIDs.
E.g.:
var myUniqueFileName = string.Format(#"{0}.txt", Guid.NewGuid());
or shorter:
var myUniqueFileName = $#"{Guid.NewGuid()}.txt";
In my programs, I sometimes try e.g. 10 times to generate a readable name ("Image1.png"…"Image10.png") and if that fails (because the file already exists), I fall back to GUIDs.
Update:
Recently, I've also use DateTime.Now.Ticks instead of GUIDs:
var myUniqueFileName = string.Format(#"{0}.txt", DateTime.Now.Ticks);
or
var myUniqueFileName = $#"{DateTime.Now.Ticks}.txt";
The benefit to me is that this generates a shorter and "nicer looking" filename, compared to GUIDs.
Please note that in some cases (e.g. when generating a lot of random names in a very short time), this might make non-unique values.
Stick to GUIDs if you want to make really sure that the file names are unique, even when transfering them to other computers.
Use
Path.GetTempFileName()
or use new GUID().
Path.GetTempFilename() on MSDN.
System.IO.Path.GetRandomFileName()
Path.GetRandomFileName() on MSDN.
If the readability of the file name isn't important, then the GUID, as suggested by many will do. However, I find that looking into a directory with 1000 GUID file names is very daunting to sort through. So I usually use a combination of a static string which gives the file name some context information, a timestamp, and GUID.
For example:
public string GenerateFileName(string context)
{
return context + "_" + DateTime.Now.ToString("yyyyMMddHHmmssfff") + "_" + Guid.NewGuid().ToString("N");
}
filename1 = GenerateFileName("MeasurementData");
filename2 = GenerateFileName("Image");
This way, when I sort by filename, it will automatically group the files by the context string and sort by timestamp.
Note that the filename limit in windows is 255 characters.
Here's an algorithm that returns a unique readable filename based on the original supplied. If the original file exists, it incrementally tries to append an index to the filename until it finds one that doesn't exist. It reads the existing filenames into a HashSet to check for collisions so it's pretty quick (a few hundred filenames per second on my machine), it's thread safe too, and doesn't suffer from race conditions.
For example, if you pass it test.txt, it will attempt to create files in this order:
test.txt
test (2).txt
test (3).txt
etc. You can specify the maximum attempts or just leave it at the default.
Here's a complete example:
class Program
{
static FileStream CreateFileWithUniqueName(string folder, string fileName,
int maxAttempts = 1024)
{
// get filename base and extension
var fileBase = Path.GetFileNameWithoutExtension(fileName);
var ext = Path.GetExtension(fileName);
// build hash set of filenames for performance
var files = new HashSet<string>(Directory.GetFiles(folder));
for (var index = 0; index < maxAttempts; index++)
{
// first try with the original filename, else try incrementally adding an index
var name = (index == 0)
? fileName
: String.Format("{0} ({1}){2}", fileBase, index, ext);
// check if exists
var fullPath = Path.Combine(folder, name);
if(files.Contains(fullPath))
continue;
// try to create the file
try
{
return new FileStream(fullPath, FileMode.CreateNew, FileAccess.Write);
}
catch (DirectoryNotFoundException) { throw; }
catch (DriveNotFoundException) { throw; }
catch (IOException)
{
// Will occur if another thread created a file with this
// name since we created the HashSet. Ignore this and just
// try with the next filename.
}
}
throw new Exception("Could not create unique filename in " + maxAttempts + " attempts");
}
static void Main(string[] args)
{
for (var i = 0; i < 500; i++)
{
using (var stream = CreateFileWithUniqueName(#"c:\temp\", "test.txt"))
{
Console.WriteLine("Created \"" + stream.Name + "\"");
}
}
Console.ReadKey();
}
}
I use GetRandomFileName:
The GetRandomFileName method returns a cryptographically strong, random string that can be used as either a folder name or a file name. Unlike GetTempFileName, GetRandomFileName does not create a file. When the security of your file system is paramount, this method should be used instead of GetTempFileName.
Example:
public static string GenerateFileName(string extension="")
{
return string.Concat(Path.GetRandomFileName().Replace(".", ""),
(!string.IsNullOrEmpty(extension)) ? (extension.StartsWith(".") ? extension : string.Concat(".", extension)) : "");
}
You can have a unique file name automatically generated for you without any custom methods. Just use the following with the StorageFolder Class or the StorageFile Class. The key here is: CreationCollisionOption.GenerateUniqueName and NameCollisionOption.GenerateUniqueName
To create a new file with a unique filename:
var myFile = await ApplicationData.Current.LocalFolder.CreateFileAsync("myfile.txt", NameCollisionOption.GenerateUniqueName);
To copy a file to a location with a unique filename:
var myFile2 = await myFile1.CopyAsync(ApplicationData.Current.LocalFolder, myFile1.Name, NameCollisionOption.GenerateUniqueName);
To move a file with a unique filename in the destination location:
await myFile.MoveAsync(ApplicationData.Current.LocalFolder, myFile.Name, NameCollisionOption.GenerateUniqueName);
To rename a file with a unique filename in the destination location:
await myFile.RenameAsync(myFile.Name, NameCollisionOption.GenerateUniqueName);
Create your timestamped filename
following your normal process
Check to see if filename exists
False - save file
True - Append additional character to file, perhaps a counter
Go to step 2
Do you need the date time stamp in the filename?
You could make the filename a GUID.
I have been using the following code and its working fine. I hope this might help you.
I begin with a unique file name using a timestamp -
"context_" + DateTime.Now.ToString("yyyyMMddHHmmssffff")
C# code -
public static string CreateUniqueFile(string logFilePath, string logFileName, string fileExt)
{
try
{
int fileNumber = 1;
//prefix with . if not already provided
fileExt = (!fileExt.StartsWith(".")) ? "." + fileExt : fileExt;
//Generate new name
while (File.Exists(Path.Combine(logFilePath, logFileName + "-" + fileNumber.ToString() + fileExt)))
fileNumber++;
//Create empty file, retry until one is created
while (!CreateNewLogfile(logFilePath, logFileName + "-" + fileNumber.ToString() + fileExt))
fileNumber++;
return logFileName + "-" + fileNumber.ToString() + fileExt;
}
catch (Exception)
{
throw;
}
}
private static bool CreateNewLogfile(string logFilePath, string logFile)
{
try
{
FileStream fs = new FileStream(Path.Combine(logFilePath, logFile), FileMode.CreateNew);
fs.Close();
return true;
}
catch (IOException) //File exists, can not create new
{
return false;
}
catch (Exception) //Exception occured
{
throw;
}
}
Why can't we make a unique id as below.
We can use DateTime.Now.Ticks and Guid.NewGuid().ToString() to combine together and make a unique id.
As the DateTime.Now.Ticks is added, we can find out the Date and Time in seconds at which the unique id is created.
Please see the code.
var ticks = DateTime.Now.Ticks;
var guid = Guid.NewGuid().ToString();
var uniqueSessionId = ticks.ToString() +'-'+ guid; //guid created by combining ticks and guid
var datetime = new DateTime(ticks);//for checking purpose
var datetimenow = DateTime.Now; //both these date times are different.
We can even take the part of ticks in unique id and check for the date and time later for future reference.
You can attach the unique id created to the filename or can be used for creating unique session id for login-logout of users to our application or website.
How about using Guid.NewGuid() to create a GUID and use that as the filename (or part of the filename together with your time stamp if you like).
I've written a simple recursive function that generates file names like Windows does, by appending a sequence number prior to the file extension.
Given a desired file path of C:\MyDir\MyFile.txt, and the file already exists, it returns a final file path of C:\MyDir\MyFile_1.txt.
It is called like this:
var desiredPath = #"C:\MyDir\MyFile.txt";
var finalPath = UniqueFileName(desiredPath);
private static string UniqueFileName(string path, int count = 0)
{
if (count == 0)
{
if (!File.Exists(path))
{
return path;
}
}
else
{
var candidatePath = string.Format(
#"{0}\{1}_{2}{3}",
Path.GetDirectoryName(path),
Path.GetFileNameWithoutExtension(path),
count,
Path.GetExtension(path));
if (!File.Exists(candidatePath))
{
return candidatePath;
}
}
count++;
return UniqueFileName(path, count);
}
DateTime.Now.Ticks is not safe, Guid.NewGuid() is too ugly, if you need something clean and almost safe (it's not 100% safe for example if you call it 1,000,000 times in 1ms), try:
Math.Abs(Guid.NewGuid().GetHashCode())
By safe I mean safe to be unique when you call it so many times in very short period few ms of time.
If you would like to have the datetime,hours,minutes etc..you can use a static variable. Append the value of this variable to the filename. You can start the counter with 0 and increment when you have created a file. This way the filename will surely be unique since you have seconds also in the file.
I usually do something along these lines:
start with a stem file name (work.dat1 for instance)
try to create it with CreateNew
if that works, you've got the file, otherwise...
mix the current date/time into the filename (work.2011-01-15T112357.dat for instance)
try to create the file
if that worked, you've got the file, otherwise...
Mix a monotonic counter into the filename (work.2011-01-15T112357.0001.dat for instance. (I dislike GUIDs. I prefer order/predictability.)
try to create the file. Keep ticking up the counter and retrying until a file gets created for you.
Here's a sample class:
static class DirectoryInfoHelpers
{
public static FileStream CreateFileWithUniqueName( this DirectoryInfo dir , string rootName )
{
FileStream fs = dir.TryCreateFile( rootName ) ; // try the simple name first
// if that didn't work, try mixing in the date/time
if ( fs == null )
{
string date = DateTime.Now.ToString( "yyyy-MM-ddTHHmmss" ) ;
string stem = Path.GetFileNameWithoutExtension(rootName) ;
string ext = Path.GetExtension(rootName) ?? ".dat" ;
ext = ext.Substring(1);
string fn = string.Format( "{0}.{1}.{2}" , stem , date , ext ) ;
fs = dir.TryCreateFile( fn ) ;
// if mixing in the date/time didn't work, try a sequential search
if ( fs == null )
{
int seq = 0 ;
do
{
fn = string.Format( "{0}.{1}.{2:0000}.{3}" , stem , date , ++seq , ext ) ;
fs = dir.TryCreateFile( fn ) ;
} while ( fs == null ) ;
}
}
return fs ;
}
private static FileStream TryCreateFile(this DirectoryInfo dir , string fileName )
{
FileStream fs = null ;
try
{
string fqn = Path.Combine( dir.FullName , fileName ) ;
fs = new FileStream( fqn , FileMode.CreateNew , FileAccess.ReadWrite , FileShare.None ) ;
}
catch ( Exception )
{
fs = null ;
}
return fs ;
}
}
You might want to tweak the algorithm (always use all the possible components to the file name for instance). Depends on the context -- If I was creating log files for instance, that I might want to rotate out of existence, you'd want them all to share the same pattern to the name.
The code isn't perfect (no checks on the data passed in for instance). And the algorithm's not perfect (if you fill up the hard drive or encounter permissions, actual I/O errors or other file system errors, for instance, this will hang, as it stands, in an infinite loop).
I ends up concatenating GUID with Day Month Year Second Millisecond string and i think this solution is quite good in my scenario
you can use Random.Next() also to generate a random number. you can see the MSDN link: http://msdn.microsoft.com/en-us/library/9b3ta19y.aspx
I wrote a class specifically for doing this. It's initialized with a "base" part (defaults to a minute-accurate timestamp) and after that appends letters to make unique names. So, if the first stamp generated is 1907101215a, the second would be 1907101215b, then 1907101215c, et cetera.
If I need more than 25 unique stamps then I use unary 'z's to count 25's. So, it goes 1907101215y, 1907101215za, 1907101215zb, ... 1907101215zy, 1907101215zza, 1907101215zzb, and so forth. This guarantees that the stamps will always sort alphanumerically in the order they were generated (as long as the next character after the stamp isn't a letter).
It isn't thread-safe, doesn't automatically update the time, and quickly bloats if you need hundreds of stamps, but I find it sufficient for my needs.
/// <summary>
/// Class for generating unique stamps (for filenames, etc.)
/// </summary>
/// <remarks>
/// Each time ToString() is called, a unique stamp is generated.
/// Stamps are guaranteed to sort alphanumerically in order of generation.
/// </remarks>
public class StampGenerator
{
/// <summary>
/// All the characters which could be the last character in the stamp.
/// </summary>
private static readonly char[] _trailingChars =
{
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y'
};
/// <summary>
/// How many valid trailing characters there are.
/// </summary>
/// <remarks>Should always equal _trailingChars.Length</remarks>
public const int TRAILING_RANGE = 25;
/// <summary>
/// Maximum length of the stamp. Hard-coded for laziness.
/// </summary>
public const int MAX_LENGTH_STAMP = 28;
/// <summary>
/// Base portion of the stamp. Will be constant between calls.
/// </summary>
/// <remarks>
/// This is intended to uniquely distinguish between instances.
/// Default behavior is to generate a minute-accurate timestamp.
/// </remarks>
public string StampBase { get; }
/// <summary>
/// Number of times this instance has been called.
/// </summary>
public int CalledTimes { get; private set; }
/// <summary>
/// Maximum number of stamps that can be generated with a given base.
/// </summary>
public int MaxCalls { get; }
/// <summary>
/// Number of stamps remaining for this instance.
/// </summary>
public int RemainingCalls { get { return MaxCalls - CalledTimes; } }
/// <summary>
/// Instantiate a StampGenerator with a specific base.
/// </summary>
/// <param name="stampBase">Base of stamp.</param>
/// <param name="calledTimes">
/// Number of times this base has already been used.
/// </param>
public StampGenerator(string stampBase, int calledTimes = 0)
{
if (stampBase == null)
{
throw new ArgumentNullException("stampBase");
}
else if (Regex.IsMatch(stampBase, "[^a-zA-Z_0-9 \\-]"))
{
throw new ArgumentException("Invalid characters in Stamp Base.",
"stampBase");
}
else if (stampBase.Length >= MAX_LENGTH_STAMP - 1)
{
throw new ArgumentException(
string.Format("Stamp Base too long. (Length {0} out of {1})",
stampBase.Length, MAX_LENGTH_STAMP - 1), "stampBase");
}
else if (calledTimes < 0)
{
throw new ArgumentOutOfRangeException(
"calledTimes", calledTimes, "calledTimes cannot be negative.");
}
else
{
int maxCalls = TRAILING_RANGE * (MAX_LENGTH_STAMP - stampBase.Length);
if (calledTimes >= maxCalls)
{
throw new ArgumentOutOfRangeException(
"calledTimes", calledTimes, string.Format(
"Called Times too large; max for stem of length {0} is {1}.",
stampBase.Length, maxCalls));
}
else
{
StampBase = stampBase;
CalledTimes = calledTimes;
MaxCalls = maxCalls;
}
}
}
/// <summary>
/// Instantiate a StampGenerator with default base string based on time.
/// </summary>
public StampGenerator() : this(DateTime.Now.ToString("yMMddHHmm")) { }
/// <summary>
/// Generate a unique stamp.
/// </summary>
/// <remarks>
/// Stamp values are orered like this:
/// a, b, ... x, y, za, zb, ... zx, zy, zza, zzb, ...
/// </remarks>
/// <returns>A unique stamp.</returns>
public override string ToString()
{
int zCount = CalledTimes / TRAILING_RANGE;
int trailing = CalledTimes % TRAILING_RANGE;
int length = StampBase.Length + zCount + 1;
if (length > MAX_LENGTH_STAMP)
{
throw new InvalidOperationException(
"Stamp length overflown! Cannot generate new stamps.");
}
else
{
CalledTimes = CalledTimes + 1;
var builder = new StringBuilder(StampBase, length);
builder.Append('z', zCount);
builder.Append(_trailingChars[trailing]);
return builder.ToString();
}
}
}
Old question, I know, but here's is what works for me. If multiple threads download files, assign each a unique number and prepend to it the filename, e.g. 01_202107210938xxxx
If you're wanting to generate a file name based off of some text like a DateTime and maybe a GUID, I have made NuGet package that allows you to do this, if you count the amount of filenames you can use that as the seed so that it is truly random. I tried to make it as straight forward and as easy to use as possible, but here's some code that you can use to generate it:
First install the NuGet package https://www.nuget.org/packages/uniqueit/
Then import it.
Finally, enter the code below:
List<string> list = new List<string>();
list.Add(new DateTime().ToString());
list.Add("Some filename or GUID");
int amountoffiles = 5000;
string final_filename = vuniqueit.Identity.GenerateUUID(list, amountoffiles));
Related
i am assigning images[] with an array that holds images file names with full path of given Directory.
string[] images = DirLoad.FileNamesArray(
IO.Loaders.PathType.full,
IO.Loaders.FileExtension.jpg
);
...now, that images[] stores all the file names i need, as I had to use the full path to get it done,
using Directory.GetFiles()
Next action requires it as a local file name.
(each is then passed as string type parameter to another method)
so my question is :
How can i omit first part - HttpRuntime.AppDomainAppPath ...if it's same in every element of array ?
this is usage example, the string is currentDir i need to trim from each element in images[]
public class IO
{
public class Loaders
{
readonly string currentDir = HttpRuntime.AppDomainAppPath;
public string selecedDirName { get; set; }
/// <summary>
/// assign The Loaders.selectedDir First before calling
/// </summary>
/// <param name="foldertoLoad"></param>
/// <returns></returns>
public enum PathType
{
full, local
}
public enum FileExtension
{
jpg,png,txt,xml,htm,js,aspx,css
}
public string[] FileNamesArray(PathType SelectedPathMode, FileExtension selectedfileType)
{
string thisFolder = "";
string thatFileType= string.Format("*.{0}",selectedfileType.ToString());
switch (SelectedPathMode)
{
case PathType.full:
thisFolder = Path.Combine(currentDir, selecedDirName);
break;
case PathType.local:
thisFolder = selecedDirName;
break;
default:
break;
}
string[] foundArr = Directory.GetFiles(thisFolder, thatFileType);
return foundArr;
}
}
}
Update , this is what i've tried
string fileName;
string[] images = DirLoad.FilesArray(IO.Loaders.PathType.full, IO.Loaders.FileExtention.jpg);
foreach (var currImage in images)
{
int startingAt = DirLoad.currentDir.Length ;
int finalPoint = currImage.Length - startingAt;
fileName = new String(currImage.ToCharArray(startingAt, finalPoint));
baseStyle.Add(string.Format("{0}url({1}) {2}", BackGroundCssProp, fileName, imageProps));
}
return baseStyle.ToArray();
Still I fail to understand, what you're trying to accomplish from the beginning to the end, but..If you are having an array of full paths and you need to get only filenames from these paths, you can do the following:
Actually files may contain random, absolutely different paths, but according to what I have caught from the question, et it be:
var files = Directory.GetFiles(#"path");
Then you may use Path.GetFileName Method to retrieve only filename from these paths, through a simple Enumerable.Select LINQ-statement:
var fileNamesOnly = files.Select(f => Path.GetFileName(f));
I am not entirely sure what you exactly need. For your sentence:
the string is currentDir i need to trim from each element in images[]
You can try the following using LINQ:
string currDir = "SomeString";
string[] images = new string[] { "SomeStringabc1.jpg", "SomeStringabc2.jpg", "SomeStringabc3.jpg", "abc.jpg" };
string[] newImages = images.Select(r => r.StartsWith(currDir)
? r.Replace(currDir, "") : r)
.ToArray();
Or using string.TrimStart
string[] newImages = images.Select(r => r.TrimStart(currDir.ToCharArray())).ToArray();
sorry but it is not clear to me... if you want only the filename from whole path then you can simply use Split for it, split the whole path with special character and use last array element.
once you will get all the path in your "images" array you can try below code.
for example:-
for(i=0;i<images.length;i++)
{
string [] cuttofilename=images[i].split('\');
string filename=cuttofilename[cuttofilename.lentgh-1];
}
I have a delimited text file: one of the columns is DocDate
The ddate column looks like this
20070222
20070221
(there is 100's of dates in this text file like this)
so it's (delimited with |)
|DDate|
|20070222|
|20070221|
I need to translate this to
|DDate|
|02/22/2007|
|02/21/2007|
i have a current replace statement that i have this text file being formated in. If someone could show me how to actually to place that inside of the statement that would be awesome.
using (StreamReader stream = new StreamReader(File.Open(#"C:\nPrep\" + textBox1.Text + "\\CI\\ncr.txt", FileMode.Open)))
{
string fileText = stream.ReadToEnd();
fileText = fileText.Replace(#"BegAtt|EndAtt", "BegAtt#|EndAtt#");
fileText = fileText.Replace(#"Cc|*RFP", "CC|RFP");
fileText = fileText.Replace(#"<swme> ", string.Empty);
fileText = fileText.Replace(#" </swme>",";");
using (StreamWriter writer = new StreamWriter(File.Open(#"C:\" + textBox1.Text + "\\nc" + "\\Data\\ncr.txt", FileMode.Create)))
{
writer.Write(fileText);
}
}
}
Example:
Before date conversion:
216442|216443|||20080823|EM
After Date:
216442|216443|||08/23/2005|EM
You can run the date strings through a method like this:
private static string ReformatDate(string input)
{
return DateTime.ParseExact(input, "|yyyyMMdd|", CultureInfo.InvariantCulture)
.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture);
}
Example:
Console.WriteLine(ReformatDate("|20070222|")); // prints 02/22/2007
Update
Complete sample including file parsing:
private const int DATE_COLUMN = 4;
static void Main(string[] args)
{
string inputFile = #"c:\temp\input.txt";
string outputFile = #"c:\temp\output.txt";
using (StreamReader reader = File.OpenText(inputFile))
using(Stream outputStream = File.OpenWrite(outputFile))
using (StreamWriter writer = new StreamWriter(outputStream))
{
do
{
string line = reader.ReadLine();
if (line == null)
{
break;
}
writer.WriteLine(TransformLine(line));
} while (true);
}
File.Delete(inputFile);
File.Move(outputFile, inputFile);
}
private static char[] separator = "|".ToCharArray();
private static string TransformLine(string line)
{
string[] columns = line.Split(separator);
columns[DATE_COLUMN] = ReformatDate(columns[4]);
return string.Join("|", columns);
}
private static string ReformatDate(string input)
{
return DateTime.ParseExact(input, "yyyyMMdd", CultureInfo.InvariantCulture)
.ToString("MM/dd/yyyy", CultureInfo.InvariantCulture);
}
Now it will have replaced the original file with one that has the transformed lines.
I think this does what you want:
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.IO;
class Program
{
static void Main(string[] args)
{
string inputFilename = "input.txt";
string outputFilename = "output.txt";
string[] dateColumnNames = { "DDate" };
using (StreamReader stream = new StreamReader(File.Open(inputFilename, FileMode.Open)))
using (StreamWriter writer = new StreamWriter(File.Open(outputFilename, FileMode.Create)))
{
int[] dateColumns = new int[0];
while (true)
{
string line = stream.ReadLine();
if (line == null)
break;
// Split into columns.
string[] columns = line.Split('|');
// Find date columns.
int[] newDateColumns =
columns.Select((name, index) => new { Name = name, Index = index })
.Where(x => dateColumnNames.Contains(x.Name))
.Select(x => x.Index)
.ToArray();
if (newDateColumns.Length > 0)
dateColumns = newDateColumns;
// Replace dates.
foreach (int i in dateColumns)
{
if (columns.Length > i)
{
Regex regex = new Regex(#"(\d{4})(\d{2})(\d{2})");
columns[i] = regex.Replace(columns[i], "$2/$3/$1");
line = string.Join("|", columns);
}
}
// Make other replacements.
line = line.Replace(#"BegAtt|EndAtt", "BegAtt#|EndAtt#");
line = line.Replace(#"Cc|*RFP", "CC|RFP");
line = line.Replace(#"<swme> ", string.Empty);
line = line.Replace(#" </swme>", ";");
// Output line.
writer.WriteLine(line);
}
}
}
}
Example input:
a|b|c|d|DDate|e
216442|20011223|||20080823|EM
216443|20011223|||20080824|EM
a|DDate|c|d|e|f
216442|20011223|||20080823|EM
<swme> Just a test </swme>
Output:
a|b|c|d|DDate|e
216442|20011223|||08/23/2008|EM
216443|20011223|||08/24/2008|EM
a|DDate|c|d|e|f
216442|12/23/2001|||20080823|EM
Just a test;
Notice that the DDate column changes. You can also specify multiple date columns if you wish. Just change the array dateColumnNames.
Honestly I wouldn't really try anything clever here, the problem is pretty straight forward.
The easiest way to do the conversion is to convert the original to a C# DateTime, and then format it again with the new style. You can use DateTime.Parse method with the format string fitting your old style, and follow it with a ToString call with a different format string.
DateTime.Parse
DateTime.ToString
DateTimeFormatInfo class -has the custom format strings
As for the general solution, I think I'd map the columns to a class with some simple properties. If memory is not an issue, then create a list of these objects from file data, format the dates, and write the objects back. I find it easier to do it this way because it's more transparent and easier to debug than complex regex statements and the like, and because it's a little more maintenance friendly.
The class would have properties mapping to columns (think of what an ORM does for a database). You can add different format options for it, and override the ToString method in creative ways, add different validation/formatting/logic rules etc.
The problem goes sort of like this then:
create a "get next token" function
grab the required number of tokens that makes up a row
pass the tokens to the class constructor, add class to list
do that until there is nothing left in the file
start a new file, or overwrite the old one
for each item in the list, call the ToString method and append the result to the file
Then if your formatting rules change, or the file format or whatever, you have a straightforward maintenance path.
Edit: For a looser schema you can use a Dictionary object in a couple of clever ways, something like this:
Get the series of tokens representing the first row. This is your mapping for column name/type and its number. You can store them in a simple array - meaning that for every line of text that you read you can look up the name - let's say ColumnArray[TokenNumber] will give you the name of the column.
For every line of text, create a Dictionary, and fill it with a series of values of the form {ColumnArray[TokenNumber], Token}. This represents your row. Add this dictionary to the list of all rows.
For every item in the list, reformat item["DDate"] to the desired value
Then write the list back to the file. First output the tokenized ColumnArray - these are your headers. Then For every row dictionary you can run a loop like this:
foreach(Dictionary<string, string> row in rowList){
foreach(string columnName in ColumnArray){
WriteToken(row[columnNmae]);
}
}
Update
I made something that's fairly generic for this purpose. Slow day today I guess :)
public class ListFormatter {
// stores transformation delegates keyed by column name (multiple keys for each column is allowed)
public List<KeyValuePair<String, Func<String, String>>> Transforms = new List<KeyValuePair<String, Func<String, String>>>();
// method for tokenizing and writing back - encapsulate file format to some extent
public Func<String, String[]> GetTokensFromLine { get; set; }
public Func<IEnumerable<String>, String> GetLineFromTokens { get; set; }
public String ReservedColumnNameAnyColumn = String.Empty;
public String ReservedColumnNameWholeLine = "WholeLine";
public ListFormatter() {
// by default let's set up for '|' delimited tokens, client can overwrite however
GetTokensFromLine = s => { return s.Split('|'); };
GetLineFromTokens = l => {
var b = new StringBuilder();
for (int i = 0; i < l.Count(); i++) {
b.Append((i > 0) ? " | " + l.ElementAt(i) : l.ElementAt(i));
}
return b.ToString();
};
}
public void FormatList(StreamReader inStream, StreamWriter outStream) {
// get the column names
var columns = GetTokensFromLine(inStream.ReadLine());
// TODO - validate that every column has a name
// write he column header to the output
outStream.WriteLine(GetLineFromTokens(columns));
// iterate through the stream
while (true) {
// get a line of text, run any transforms registered to work on the whole line
var line = RunTransforms(inStream.ReadLine(), GetRowTransforms());
if (line == null) break;
// get the row of tokens TODO - validate for number of tokens
var tokens = GetTokensFromLine(line);
// run transforms on the columns
for (var i = 0; i < tokens.Count(); i++ ) {
tokens[i] = RunTransforms(tokens[i], GetColumnTransforms(columns[i]));
}
// write the new line to the output
outStream.WriteLine(GetLineFromTokens(tokens));
}
}
/// <summary>
/// Gets the transforms associated with a single column value
/// </summary>
/// <param name="name">The name.</param>
/// <returns></returns>
public IEnumerable<Func<String, String>> GetColumnTransforms(string name) {
return from kv in Transforms where kv.Key == ReservedColumnNameAnyColumn || kv.Key == name select kv.Value;
}
/// <summary>
/// Gets the transforms associated with the whole row
/// </summary>
/// <returns></returns>
public IEnumerable<Func<String, String>> GetRowTransforms() {
return from kv in Transforms where kv.Key == ReservedColumnNameWholeLine select kv.Value;
}
/// <summary>
/// Runs the transforms on a string
/// </summary>
/// <param name="item">The item.</param>
/// <param name="transformList">The transform list.</param>
/// <returns></returns>
public string RunTransforms(string item, IEnumerable<Func<String, String>> transformList) {
if (item != null) {
foreach (var func in transformList) {
item = func(item);
}
}
return item;
}
}
// usage example
public void FormatList() {
var formatter = new ListFormatter();
// add some rules
// formats every line of text
formatter.Transforms.Add(new KeyValuePair<string, Func<string, string>>(formatter.ReservedColumnNameWholeLine, s => s.Trim()));
// format every column entry
formatter.Transforms.Add(new KeyValuePair<string, Func<string, string>>(formatter.ReservedColumnNameAnyColumn, s => s.Trim()));
// format that date
formatter.Transforms.Add(new KeyValuePair<string, Func<string, string>>("DDate", s => DateTime.ParseExact(s, "oldformat", CultureInfo.InvariantCulture).ToString("newformat")));
// format
using (var reader = File.OpenText("infile"))
using(var outputStream = new StreamWriter(File.OpenWrite("outfile"))) {
formatter.FormatList(reader, outputStream);
}
}
Allows you to add arbitrary number of rules for a specific column, all columns, and whole line of text. Uses your delimiter characters by default, but can be overridden however. The actual formatter class works on streams, so any buffer and file management stuff is left up to the client.
The idea is to encapsulate the core functionality into something simple and reusable. So, for example, to add you other text replacements you simply add another rule that works on the whole line of text, or every column value separately, whichever fits the situation. The actual rules are separate from the formatting process and can be tested individually. Here is how you could configure your other replacements:
formatter.Transforms.Add(new KeyValuePair<string, Func<string, string>>(formatter.ReservedColumnNameWholeLine, s => {
// Make other replacements.
s = s.Replace(#"BegAtt|EndAtt", "BegAtt#|EndAtt#");
s = s.Replace(#"Cc|*RFP", "CC|RFP");
s = s.Replace(#"<swme> ", string.Empty);
s = s.Replace(#" </swme>", ";");
return s;
}));
I have to make a loop to generate a 5 randomly-picked-letter string, and then create a text file under that name, lets say in C:// , how will I do that? Both the generating name and creating a file in the directory. I think I have to pick 5 random numbers from the ascii codes add them to an array and then convert them to the character equivalents to be able to use it as a name. Idk how I'll convert them to character and make up a string with them, could you help me?
Look at the GetTempFileName and GetRandomFileName methods of the System.IO.Path class.
GetRandomFileName creates a "cryptographically strong" file name, and is the closer one to what you asked for.
GetTempFileName creates a unique file name in a directory -- and also creates a zero-byte file with that name too -- helping ensure its uniqueness. This might be closer to what you might actually need.
If you want to create the file names youself, put the characters that you want to use in a string and pick from that:
// selected characters
string chars = "2346789ABCDEFGHJKLMNPQRTUVWXYZabcdefghjkmnpqrtuvwxyz";
// create random generator
Random rnd = new Random();
string name;
do {
// create name
name = string.Empty;
while (name.Length < 5) {
name += chars.Substring(rnd.Next(chars.Length), 1);
}
// add extension
name += ".txt";
// check against files in the folder
} while (File.Exists(Path.Compbine(folderPath, name)))
What about Path.GetTempFileName() or Path.GetRandomFileName() methods? Consider also the fact that file system is not transactional and two parallel processes can create the same file name. TempFileName() should return unique name (by specification), so 'maybe' you don't need to care about that if the temp directory could be fine for your solution.
Or you could use a GUID for generating a unique filename:
Wikipedia:
While each generated GUID is not
guaranteed to be unique, the total
number of unique keys (2^128 or
3.4×10^38) is so large that the probability of the same number being
generated twice is infinitesimally small.
string text = "Sample...";
string path = "D:\\Temp\\";
if (!path.EndsWith("\\"))
path += "\\";
string filename = path + Guid.NewGuid().ToString() + ".txt";
while (File.Exists(filename))
filename = path + Guid.NewGuid().ToString() + ".txt";
TextWriter writer = null;
try
{
writer = new StreamWriter(filename);
writer.WriteLine(text);
writer.Close();
}
catch (Exception e)
{
MessageBox.Show("Exception occured: " + e.ToString());
}
finally
{
if (writer != null)
writer.Close();
}
A piece of code that generates a random string (of letters) was posted here. Code that creates files (also using random file names) is available here.
found this on Google for you (source here):
/// <summary>
/// Generates a random string with the given length
/// </summary>
/// <param name="size">Size of the string</param>
/// <param name="lowerCase">If true, generate lowercase string</param>
/// <returns>Random string</returns>
private string RandomString(int size, bool lowerCase)
{
StringBuilder builder = new StringBuilder();
Random random = new Random();
char ch ;
for(int i=0; i<size; i++)
{
ch = Convert.ToChar(Convert.ToInt32(Math.Floor(26 * random.NextDouble() + 65))) ;
builder.Append(ch);
}
if(lowerCase)
return builder.ToString().ToLower();
return builder.ToString();
}
After that, use file streams to create and write the file.
I want to include a batch file rename functionality in my application. A user can type a destination filename pattern and (after replacing some wildcards in the pattern) I need to check if it's going to be a legal filename under Windows. I've tried to use regular expression like [a-zA-Z0-9_]+ but it doesn't include many national-specific characters from various languages (e.g. umlauts and so on). What is the best way to do such a check?
From MSDN's "Naming a File or Directory," here are the general conventions for what a legal file name is under Windows:
You may use any character in the current code page (Unicode/ANSI above 127), except:
< > : " / \ | ? *
Characters whose integer representations are 0-31 (less than ASCII space)
Any other character that the target file system does not allow (say, trailing periods or spaces)
Any of the DOS names: CON, PRN, AUX, NUL, COM0, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, LPT0, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, LPT9 (and avoid AUX.txt, etc)
The file name is all periods
Some optional things to check:
File paths (including the file name) may not have more than 260 characters (that don't use the \?\ prefix)
Unicode file paths (including the file name) with more than 32,000 characters when using \?\ (note that prefix may expand directory components and cause it to overflow the 32,000 limit)
You can get a list of invalid characters from Path.GetInvalidPathChars and GetInvalidFileNameChars.
UPD: See Steve Cooper's suggestion on how to use these in a regular expression.
UPD2: Note that according to the Remarks section in MSDN "The array returned from this method is not guaranteed to contain the complete set of characters that are invalid in file and directory names." The answer provided by sixlettervaliables goes into more details.
For .Net Frameworks prior to 3.5 this should work:
Regular expression matching should get you some of the way. Here's a snippet using the System.IO.Path.InvalidPathChars constant;
bool IsValidFilename(string testName)
{
Regex containsABadCharacter = new Regex("["
+ Regex.Escape(System.IO.Path.InvalidPathChars) + "]");
if (containsABadCharacter.IsMatch(testName)) { return false; };
// other checks for UNC, drive-path format, etc
return true;
}
For .Net Frameworks after 3.0 this should work:
http://msdn.microsoft.com/en-us/library/system.io.path.getinvalidpathchars(v=vs.90).aspx
Regular expression matching should get you some of the way. Here's a snippet using the System.IO.Path.GetInvalidPathChars() constant;
bool IsValidFilename(string testName)
{
Regex containsABadCharacter = new Regex("["
+ Regex.Escape(new string(System.IO.Path.GetInvalidPathChars())) + "]");
if (containsABadCharacter.IsMatch(testName)) { return false; };
// other checks for UNC, drive-path format, etc
return true;
}
Once you know that, you should also check for different formats, eg c:\my\drive and \\server\share\dir\file.ext
Try to use it, and trap for the error. The allowed set may change across file systems, or across different versions of Windows. In other words, if you want know if Windows likes the name, hand it the name and let it tell you.
This class cleans filenames and paths; use it like
var myCleanPath = PathSanitizer.SanitizeFilename(myBadPath, ' ');
Here's the code;
/// <summary>
/// Cleans paths of invalid characters.
/// </summary>
public static class PathSanitizer
{
/// <summary>
/// The set of invalid filename characters, kept sorted for fast binary search
/// </summary>
private readonly static char[] invalidFilenameChars;
/// <summary>
/// The set of invalid path characters, kept sorted for fast binary search
/// </summary>
private readonly static char[] invalidPathChars;
static PathSanitizer()
{
// set up the two arrays -- sorted once for speed.
invalidFilenameChars = System.IO.Path.GetInvalidFileNameChars();
invalidPathChars = System.IO.Path.GetInvalidPathChars();
Array.Sort(invalidFilenameChars);
Array.Sort(invalidPathChars);
}
/// <summary>
/// Cleans a filename of invalid characters
/// </summary>
/// <param name="input">the string to clean</param>
/// <param name="errorChar">the character which replaces bad characters</param>
/// <returns></returns>
public static string SanitizeFilename(string input, char errorChar)
{
return Sanitize(input, invalidFilenameChars, errorChar);
}
/// <summary>
/// Cleans a path of invalid characters
/// </summary>
/// <param name="input">the string to clean</param>
/// <param name="errorChar">the character which replaces bad characters</param>
/// <returns></returns>
public static string SanitizePath(string input, char errorChar)
{
return Sanitize(input, invalidPathChars, errorChar);
}
/// <summary>
/// Cleans a string of invalid characters.
/// </summary>
/// <param name="input"></param>
/// <param name="invalidChars"></param>
/// <param name="errorChar"></param>
/// <returns></returns>
private static string Sanitize(string input, char[] invalidChars, char errorChar)
{
// null always sanitizes to null
if (input == null) { return null; }
StringBuilder result = new StringBuilder();
foreach (var characterToTest in input)
{
// we binary search for the character in the invalid set. This should be lightning fast.
if (Array.BinarySearch(invalidChars, characterToTest) >= 0)
{
// we found the character in the array of
result.Append(errorChar);
}
else
{
// the character was not found in invalid, so it is valid.
result.Append(characterToTest);
}
}
// we're done.
return result.ToString();
}
}
This is what I use:
public static bool IsValidFileName(this string expression, bool platformIndependent)
{
string sPattern = #"^(?!^(PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d|\..*)(\..+)?$)[^\x00-\x1f\\?*:\"";|/]+$";
if (platformIndependent)
{
sPattern = #"^(([a-zA-Z]:|\\)\\)?(((\.)|(\.\.)|([^\\/:\*\?""\|<>\. ](([^\\/:\*\?""\|<>\. ])|([^\\/:\*\?""\|<>]*[^\\/:\*\?""\|<>\. ]))?))\\)*[^\\/:\*\?""\|<>\. ](([^\\/:\*\?""\|<>\. ])|([^\\/:\*\?""\|<>]*[^\\/:\*\?""\|<>\. ]))?$";
}
return (Regex.IsMatch(expression, sPattern, RegexOptions.CultureInvariant));
}
The first pattern creates a regular expression containing the invalid/illegal file names and characters for Windows platforms only. The second one does the same but ensures that the name is legal for any platform.
One corner case to keep in mind, which surprised me when I first found out about it: Windows allows leading space characters in file names! For example, the following are all legal, and distinct, file names on Windows (minus the quotes):
"file.txt"
" file.txt"
" file.txt"
One takeaway from this: Use caution when writing code that trims leading/trailing whitespace from a filename string.
Simplifying the Eugene Katz's answer:
bool IsFileNameCorrect(string fileName){
return !fileName.Any(f=>Path.GetInvalidFileNameChars().Contains(f))
}
Or
bool IsFileNameCorrect(string fileName){
return fileName.All(f=>!Path.GetInvalidFileNameChars().Contains(f))
}
Microsoft Windows: Windows kernel forbids the use of characters in range 1-31 (i.e., 0x01-0x1F) and characters " * : < > ? \ |. Although NTFS allows each path component (directory or filename) to be 255 characters long and paths up to about 32767 characters long, the Windows kernel only supports paths up to 259 characters long. Additionally, Windows forbids the use of the MS-DOS device names AUX, CLOCK$, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, CON, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, LPT9, NUL and PRN, as well as these names with any extension (for example, AUX.txt), except when using Long UNC paths (ex. \.\C:\nul.txt or \?\D:\aux\con). (In fact, CLOCK$ may be used if an extension is provided.) These restrictions only apply to Windows - Linux, for example, allows use of " * : < > ? \ | even in NTFS.
Source: http://en.wikipedia.org/wiki/Filename
Rather than explicitly include all possible characters, you could do a regex to check for the presence of illegal characters, and report an error then. Ideally your application should name the files exactly as the user wishes, and only cry foul if it stumbles across an error.
The question is are you trying to determine if a path name is a legal windows path, or if it's legal on the system where the code is running.? I think the latter is more important, so personally, I'd probably decompose the full path and try to use _mkdir to create the directory the file belongs in, then try to create the file.
This way you know not only if the path contains only valid windows characters, but if it actually represents a path that can be written by this process.
I use this to get rid of invalid characters in filenames without throwing exceptions:
private static readonly Regex InvalidFileRegex = new Regex(
string.Format("[{0}]", Regex.Escape(#"<>:""/\|?*")));
public static string SanitizeFileName(string fileName)
{
return InvalidFileRegex.Replace(fileName, string.Empty);
}
Also CON, PRN, AUX, NUL, COM# and a few others are never legal filenames in any directory with any extension.
To complement the other answers, here are a couple of additional edge cases that you might want to consider.
Excel can have problems if you save a workbook in a file whose name contains the '[' or ']' characters. See http://support.microsoft.com/kb/215205 for details.
Sharepoint has a whole additional set of restrictions. See http://support.microsoft.com/kb/905231 for details.
From MSDN, here's a list of characters that aren't allowed:
Use almost any character in the current code page for a name, including Unicode characters and characters in the extended character set (128–255), except for the following:
The following reserved characters are not allowed:
< > : " / \ | ? *
Characters whose integer representations are in the range from zero through 31 are not allowed.
Any other character that the target file system does not allow.
This is an already answered question, but just for the sake of "Other options", here's a non-ideal one:
(non-ideal because using Exceptions as flow control is a "Bad Thing", generally)
public static bool IsLegalFilename(string name)
{
try
{
var fileInfo = new FileInfo(name);
return true;
}
catch
{
return false;
}
}
Also the destination file system is important.
Under NTFS, some files can not be created in specific directories.
E.G. $Boot in root
Regular expressions are overkill for this situation. You can use the String.IndexOfAny() method in combination with Path.GetInvalidPathChars() and Path.GetInvalidFileNameChars().
Also note that both Path.GetInvalidXXX() methods clone an internal array and return the clone. So if you're going to be doing this a lot (thousands and thousands of times) you can cache a copy of the invalid chars array for reuse.
many of these answers will not work if the filename is too long & running on a pre Windows 10 environment. Similarly, have a think about what you want to do with periods - allowing leading or trailing is technically valid, but can create problems if you do not want the file to be difficult to see or delete respectively.
This is a validation attribute I created to check for a valid filename.
public class ValidFileNameAttribute : ValidationAttribute
{
public ValidFileNameAttribute()
{
RequireExtension = true;
ErrorMessage = "{0} is an Invalid Filename";
MaxLength = 255; //superseeded in modern windows environments
}
public override bool IsValid(object value)
{
//http://stackoverflow.com/questions/422090/in-c-sharp-check-that-filename-is-possibly-valid-not-that-it-exists
var fileName = (string)value;
if (string.IsNullOrEmpty(fileName)) { return true; }
if (fileName.IndexOfAny(Path.GetInvalidFileNameChars()) > -1 ||
(!AllowHidden && fileName[0] == '.') ||
fileName[fileName.Length - 1]== '.' ||
fileName.Length > MaxLength)
{
return false;
}
string extension = Path.GetExtension(fileName);
return (!RequireExtension || extension != string.Empty)
&& (ExtensionList==null || ExtensionList.Contains(extension));
}
private const string _sepChar = ",";
private IEnumerable<string> ExtensionList { get; set; }
public bool AllowHidden { get; set; }
public bool RequireExtension { get; set; }
public int MaxLength { get; set; }
public string AllowedExtensions {
get { return string.Join(_sepChar, ExtensionList); }
set {
if (string.IsNullOrEmpty(value))
{ ExtensionList = null; }
else {
ExtensionList = value.Split(new char[] { _sepChar[0] })
.Select(s => s[0] == '.' ? s : ('.' + s))
.ToList();
}
} }
public override bool RequiresValidationContext => false;
}
and the tests
[TestMethod]
public void TestFilenameAttribute()
{
var rxa = new ValidFileNameAttribute();
Assert.IsFalse(rxa.IsValid("pptx."));
Assert.IsFalse(rxa.IsValid("pp.tx."));
Assert.IsFalse(rxa.IsValid("."));
Assert.IsFalse(rxa.IsValid(".pp.tx"));
Assert.IsFalse(rxa.IsValid(".pptx"));
Assert.IsFalse(rxa.IsValid("pptx"));
Assert.IsFalse(rxa.IsValid("a/abc.pptx"));
Assert.IsFalse(rxa.IsValid("a\\abc.pptx"));
Assert.IsFalse(rxa.IsValid("c:abc.pptx"));
Assert.IsFalse(rxa.IsValid("c<abc.pptx"));
Assert.IsTrue(rxa.IsValid("abc.pptx"));
rxa = new ValidFileNameAttribute { AllowedExtensions = ".pptx" };
Assert.IsFalse(rxa.IsValid("abc.docx"));
Assert.IsTrue(rxa.IsValid("abc.pptx"));
}
If you're only trying to check if a string holding your file name/path has any invalid characters, the fastest method I've found is to use Split() to break up the file name into an array of parts wherever there's an invalid character. If the result is only an array of 1, there are no invalid characters. :-)
var nameToTest = "Best file name \"ever\".txt";
bool isInvalidName = nameToTest.Split(System.IO.Path.GetInvalidFileNameChars()).Length > 1;
var pathToTest = "C:\\My Folder <secrets>\\";
bool isInvalidPath = pathToTest.Split(System.IO.Path.GetInvalidPathChars()).Length > 1;
I tried running this and other methods mentioned above on a file/path name 1,000,000 times in LinqPad.
Using Split() is only ~850ms.
Using Regex("[" + Regex.Escape(new string(System.IO.Path.GetInvalidPathChars())) + "]") is around 6 seconds.
The more complicated regular expressions fair MUCH worse, as do some of the other options, like using the various methods on the Path class to get file name and let their internal validation do the job (most likely due to the overhead of exception handling).
Granted it's not very often you need to validation 1 million file names, so a single iteration is fine for most of these methods anyway. But it's still pretty efficient and effective if you're only looking for invalid characters.
I got this idea from someone. - don't know who. Let the OS do the heavy lifting.
public bool IsPathFileNameGood(string fname)
{
bool rc = Constants.Fail;
try
{
this._stream = new StreamWriter(fname, true);
rc = Constants.Pass;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Problem opening file");
rc = Constants.Fail;
}
return rc;
}
Windows filenames are pretty unrestrictive, so really it might not even be that much of an issue. The characters that are disallowed by Windows are:
\ / : * ? " < > |
You could easily write an expression to check if those characters are present. A better solution though would be to try and name the files as the user wants, and alert them when a filename doesn't stick.
I suggest just use the Path.GetFullPath()
string tagetFileFullNameToBeChecked;
try
{
Path.GetFullPath(tagetFileFullNameToBeChecked)
}
catch(AugumentException ex)
{
// invalid chars found
}
My attempt:
using System.IO;
static class PathUtils
{
public static string IsValidFullPath([NotNull] string fullPath)
{
if (string.IsNullOrWhiteSpace(fullPath))
return "Path is null, empty or white space.";
bool pathContainsInvalidChars = fullPath.IndexOfAny(Path.GetInvalidPathChars()) != -1;
if (pathContainsInvalidChars)
return "Path contains invalid characters.";
string fileName = Path.GetFileName(fullPath);
if (fileName == "")
return "Path must contain a file name.";
bool fileNameContainsInvalidChars = fileName.IndexOfAny(Path.GetInvalidFileNameChars()) != -1;
if (fileNameContainsInvalidChars)
return "File name contains invalid characters.";
if (!Path.IsPathRooted(fullPath))
return "The path must be absolute.";
return "";
}
}
This is not perfect because Path.GetInvalidPathChars does not return the complete set of characters that are invalid in file and directory names and of course there's plenty more subtleties.
So I use this method as a complement:
public static bool TestIfFileCanBeCreated([NotNull] string fullPath)
{
if (string.IsNullOrWhiteSpace(fullPath))
throw new ArgumentException("Value cannot be null or whitespace.", "fullPath");
string directoryName = Path.GetDirectoryName(fullPath);
if (directoryName != null) Directory.CreateDirectory(directoryName);
try
{
using (new FileStream(fullPath, FileMode.CreateNew)) { }
File.Delete(fullPath);
return true;
}
catch (IOException)
{
return false;
}
}
It tries to create the file and return false if there is an exception. Of course, I need to create the file but I think it's the safest way to do that. Please also note that I am not deleting directories that have been created.
You can also use the first method to do basic validation, and then handle carefully the exceptions when the path is used.
This check
static bool IsValidFileName(string name)
{
return
!string.IsNullOrWhiteSpace(name) &&
name.IndexOfAny(Path.GetInvalidFileNameChars()) < 0 &&
!Path.GetFullPath(name).StartsWith(#"\\.\");
}
filters out names with invalid chars (<>:"/\|?* and ASCII 0-31), as well as reserved DOS devices (CON, NUL, COMx). It allows leading spaces and all-dot-names, consistent with Path.GetFullPath. (Creating file with leading spaces succeeds on my system).
Used .NET Framework 4.7.1, tested on Windows 7.
One liner for verifying illigal chars in the string:
public static bool IsValidFilename(string testName) => !Regex.IsMatch(testName, "[" + Regex.Escape(new string(System.IO.Path.InvalidPathChars)) + "]");
In my opinion, the only proper answer to this question is to try to use the path and let the OS and filesystem validate it. Otherwise you are just reimplementing (and probably poorly) all the validation rules that the OS and filesystem already use and if those rules are changed in the future you will have to change your code to match them.
Is there any way to format a string by name rather than position in C#?
In python, I can do something like this example (shamelessly stolen from here):
>>> print '%(language)s has %(#)03d quote types.' % \
{'language': "Python", "#": 2}
Python has 002 quote types.
Is there any way to do this in C#? Say for instance:
String.Format("{some_variable}: {some_other_variable}", ...);
Being able to do this using a variable name would be nice, but a dictionary is acceptable too.
There is no built-in method for handling this.
Here's one method
string myString = "{foo} is {bar} and {yadi} is {yada}".Inject(o);
Here's another
Status.Text = "{UserName} last logged in at {LastLoginDate}".FormatWith(user);
A third improved method partially based on the two above, from Phil Haack
Update: This is now built-in as of C# 6 (released in 2015).
String Interpolation
$"{some_variable}: {some_other_variable}"
I have an implementation I just posted to my blog here: http://haacked.com/archive/2009/01/04/fun-with-named-formats-string-parsing-and-edge-cases.aspx
It addresses some issues that these other implementations have with brace escaping. The post has details. It does the DataBinder.Eval thing too, but is still very fast.
Interpolated strings were added into C# 6.0 and Visual Basic 14
Both were introduced through new Roslyn compiler in Visual Studio 2015.
C# 6.0:
return "\{someVariable} and also \{someOtherVariable}" OR
return $"{someVariable} and also {someOtherVariable}"
source: what's new in C#6.0
VB 14:
return $"{someVariable} and also {someOtherVariable}"
source: what's new in VB 14
Noteworthy features (in Visual Studio 2015 IDE):
syntax coloring is supported - variables contained in strings are highlighted
refactoring is supported - when renaming, variables contained in strings get renamed, too
actually not only variable names, but expressions are supported - e.g. not only {index} works, but also {(index + 1).ToString().Trim()}
Enjoy! (& click "Send a Smile" in the VS)
You can also use anonymous types like this:
public string Format(string input, object p)
{
foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(p))
input = input.Replace("{" + prop.Name + "}", (prop.GetValue(p) ?? "(null)").ToString());
return input;
}
Of course it would require more code if you also want to parse formatting, but you can format a string using this function like:
Format("test {first} and {another}", new { first = "something", another = "something else" })
There doesn't appear to be a way to do this out of the box. Though, it looks feasible to implement your own IFormatProvider that links to an IDictionary for values.
var Stuff = new Dictionary<string, object> {
{ "language", "Python" },
{ "#", 2 }
};
var Formatter = new DictionaryFormatProvider();
// Interpret {0:x} where {0}=IDictionary and "x" is hash key
Console.WriteLine string.Format(Formatter, "{0:language} has {0:#} quote types", Stuff);
Outputs:
Python has 2 quote types
The caveat is that you can't mix FormatProviders, so the fancy text formatting can't be used at the same time.
The framework itself does not provide a way to do this, but you can take a look at this post by Scott Hanselman. Example usage:
Person p = new Person();
string foo = p.ToString("{Money:C} {LastName}, {ScottName} {BirthDate}");
Assert.AreEqual("$3.43 Hanselman, {ScottName} 1/22/1974 12:00:00 AM", foo);
This code by James Newton-King is similar and works with sub-properties and indexes,
string foo = "Top result for {Name} was {Results[0].Name}".FormatWith(student));
James's code relies on System.Web.UI.DataBinder to parse the string and requires referencing System.Web, which some people don't like to do in non-web applications.
EDIT: Oh and they work nicely with anonymous types, if you don't have an object with properties ready for it:
string name = ...;
DateTime date = ...;
string foo = "{Name} - {Birthday}".FormatWith(new { Name = name, Birthday = date });
See https://stackoverflow.com/questions/271398?page=2#358259
With the linked-to extension you can write this:
var str = "{foo} {bar} {baz}".Format(foo=>"foo", bar=>2, baz=>new object());
and you'll get "foo 2 System.Object".
I think the closest you'll get is an indexed format:
String.Format("{0} has {1} quote types.", "C#", "1");
There's also String.Replace(), if you're willing to do it in multiple steps and take it on faith that you won't find your 'variables' anywhere else in the string:
string MyString = "{language} has {n} quote types.";
MyString = MyString.Replace("{language}", "C#").Replace("{n}", "1");
Expanding this to use a List:
List<KeyValuePair<string, string>> replacements = GetFormatDictionary();
foreach (KeyValuePair<string, string> item in replacements)
{
MyString = MyString.Replace(item.Key, item.Value);
}
You could do that with a Dictionary<string, string> too by iterating it's .Keys collections, but by using a List<KeyValuePair<string, string>> we can take advantage of the List's .ForEach() method and condense it back to a one-liner:
replacements.ForEach(delegate(KeyValuePair<string,string>) item) { MyString = MyString.Replace(item.Key, item.Value);});
A lambda would be even simpler, but I'm still on .Net 2.0. Also note that the .Replace() performance isn't stellar when used iteratively, since strings in .Net are immutable. Also, this requires the MyString variable be defined in such a way that it's accessible to the delegate, so it's not perfect yet.
My open source library, Regextra, supports named formatting (amongst other things). It currently targets .NET 4.0+ and is available on NuGet. I also have an introductory blog post about it: Regextra: helping you reduce your (problems){2}.
The named formatting bit supports:
Basic formatting
Nested properties formatting
Dictionary formatting
Escaping of delimiters
Standard/Custom/IFormatProvider string formatting
Example:
var order = new
{
Description = "Widget",
OrderDate = DateTime.Now,
Details = new
{
UnitPrice = 1500
}
};
string template = "We just shipped your order of '{Description}', placed on {OrderDate:d}. Your {{credit}} card will be billed {Details.UnitPrice:C}.";
string result = Template.Format(template, order);
// or use the extension: template.FormatTemplate(order);
Result:
We just shipped your order of 'Widget', placed on 2/28/2014. Your {credit} card will be billed $1,500.00.
Check out the project's GitHub link (above) and wiki for other examples.
private static Regex s_NamedFormatRegex = new Regex(#"\{(?!\{)(?<key>[\w]+)(:(?<fmt>(\{\{|\}\}|[^\{\}])*)?)?\}", RegexOptions.Compiled);
public static StringBuilder AppendNamedFormat(this StringBuilder builder,IFormatProvider provider, string format, IDictionary<string, object> args)
{
if (builder == null) throw new ArgumentNullException("builder");
var str = s_NamedFormatRegex.Replace(format, (mt) => {
string key = mt.Groups["key"].Value;
string fmt = mt.Groups["fmt"].Value;
object value = null;
if (args.TryGetValue(key,out value)) {
return string.Format(provider, "{0:" + fmt + "}", value);
} else {
return mt.Value;
}
});
builder.Append(str);
return builder;
}
public static StringBuilder AppendNamedFormat(this StringBuilder builder, string format, IDictionary<string, object> args)
{
if (builder == null) throw new ArgumentNullException("builder");
return builder.AppendNamedFormat(null, format, args);
}
Example:
var builder = new StringBuilder();
builder.AppendNamedFormat(
#"你好,{Name},今天是{Date:yyyy/MM/dd}, 这是你第{LoginTimes}次登录,积分{Score:{{ 0.00 }}}",
new Dictionary<string, object>() {
{ "Name", "wayjet" },
{ "LoginTimes",18 },
{ "Score", 100.4 },
{ "Date",DateTime.Now }
});
Output:
你好,wayjet,今天是2011-05-04, 这是你第18次登录,积分{ 100.40 }
Check this one:
public static string StringFormat(string format, object source)
{
var matches = Regex.Matches(format, #"\{(.+?)\}");
List<string> keys = (from Match matche in matches select matche.Groups[1].Value).ToList();
return keys.Aggregate(
format,
(current, key) =>
{
int colonIndex = key.IndexOf(':');
return current.Replace(
"{" + key + "}",
colonIndex > 0
? DataBinder.Eval(source, key.Substring(0, colonIndex), "{0:" + key.Substring(colonIndex + 1) + "}")
: DataBinder.Eval(source, key).ToString());
});
}
Sample:
string format = "{foo} is a {bar} is a {baz} is a {qux:#.#} is a really big {fizzle}";
var o = new { foo = 123, bar = true, baz = "this is a test", qux = 123.45, fizzle = DateTime.Now };
Console.WriteLine(StringFormat(format, o));
Performance is pretty ok compared to other solutions.
I doubt this will be possible. The first thing that comes to mind is how are you going to get access to local variable names?
There might be some clever way using LINQ and Lambda expressions to do this however.
Here's one I made a while back. It extends String with a Format method taking a single argument. The nice thing is that it'll use the standard string.Format if you provide a simple argument like an int, but if you use something like anonymous type it'll work too.
Example usage:
"The {Name} family has {Children} children".Format(new { Children = 4, Name = "Smith" })
Would result in "The Smith family has 4 children."
It doesn't do crazy binding stuff like arrays and indexers. But it is super simple and high performance.
public static class AdvancedFormatString
{
/// <summary>
/// An advanced version of string.Format. If you pass a primitive object (string, int, etc), it acts like the regular string.Format. If you pass an anonmymous type, you can name the paramters by property name.
/// </summary>
/// <param name="formatString"></param>
/// <param name="arg"></param>
/// <returns></returns>
/// <example>
/// "The {Name} family has {Children} children".Format(new { Children = 4, Name = "Smith" })
///
/// results in
/// "This Smith family has 4 children
/// </example>
public static string Format(this string formatString, object arg, IFormatProvider format = null)
{
if (arg == null)
return formatString;
var type = arg.GetType();
if (Type.GetTypeCode(type) != TypeCode.Object || type.IsPrimitive)
return string.Format(format, formatString, arg);
var properties = TypeDescriptor.GetProperties(arg);
return formatString.Format((property) =>
{
var value = properties[property].GetValue(arg);
return Convert.ToString(value, format);
});
}
public static string Format(this string formatString, Func<string, string> formatFragmentHandler)
{
if (string.IsNullOrEmpty(formatString))
return formatString;
Fragment[] fragments = GetParsedFragments(formatString);
if (fragments == null || fragments.Length == 0)
return formatString;
return string.Join(string.Empty, fragments.Select(fragment =>
{
if (fragment.Type == FragmentType.Literal)
return fragment.Value;
else
return formatFragmentHandler(fragment.Value);
}).ToArray());
}
private static Fragment[] GetParsedFragments(string formatString)
{
Fragment[] fragments;
if ( parsedStrings.TryGetValue(formatString, out fragments) )
{
return fragments;
}
lock (parsedStringsLock)
{
if ( !parsedStrings.TryGetValue(formatString, out fragments) )
{
fragments = Parse(formatString);
parsedStrings.Add(formatString, fragments);
}
}
return fragments;
}
private static Object parsedStringsLock = new Object();
private static Dictionary<string,Fragment[]> parsedStrings = new Dictionary<string,Fragment[]>(StringComparer.Ordinal);
const char OpeningDelimiter = '{';
const char ClosingDelimiter = '}';
/// <summary>
/// Parses the given format string into a list of fragments.
/// </summary>
/// <param name="format"></param>
/// <returns></returns>
static Fragment[] Parse(string format)
{
int lastCharIndex = format.Length - 1;
int currFragEndIndex;
Fragment currFrag = ParseFragment(format, 0, out currFragEndIndex);
if (currFragEndIndex == lastCharIndex)
{
return new Fragment[] { currFrag };
}
List<Fragment> fragments = new List<Fragment>();
while (true)
{
fragments.Add(currFrag);
if (currFragEndIndex == lastCharIndex)
{
break;
}
currFrag = ParseFragment(format, currFragEndIndex + 1, out currFragEndIndex);
}
return fragments.ToArray();
}
/// <summary>
/// Finds the next delimiter from the starting index.
/// </summary>
static Fragment ParseFragment(string format, int startIndex, out int fragmentEndIndex)
{
bool foundEscapedDelimiter = false;
FragmentType type = FragmentType.Literal;
int numChars = format.Length;
for (int i = startIndex; i < numChars; i++)
{
char currChar = format[i];
bool isOpenBrace = currChar == OpeningDelimiter;
bool isCloseBrace = isOpenBrace ? false : currChar == ClosingDelimiter;
if (!isOpenBrace && !isCloseBrace)
{
continue;
}
else if (i < (numChars - 1) && format[i + 1] == currChar)
{//{{ or }}
i++;
foundEscapedDelimiter = true;
}
else if (isOpenBrace)
{
if (i == startIndex)
{
type = FragmentType.FormatItem;
}
else
{
if (type == FragmentType.FormatItem)
throw new FormatException("Two consequtive unescaped { format item openers were found. Either close the first or escape any literals with another {.");
//curr character is the opening of a new format item. so we close this literal out
string literal = format.Substring(startIndex, i - startIndex);
if (foundEscapedDelimiter)
literal = ReplaceEscapes(literal);
fragmentEndIndex = i - 1;
return new Fragment(FragmentType.Literal, literal);
}
}
else
{//close bracket
if (i == startIndex || type == FragmentType.Literal)
throw new FormatException("A } closing brace existed without an opening { brace.");
string formatItem = format.Substring(startIndex + 1, i - startIndex - 1);
if (foundEscapedDelimiter)
formatItem = ReplaceEscapes(formatItem);//a format item with a { or } in its name is crazy but it could be done
fragmentEndIndex = i;
return new Fragment(FragmentType.FormatItem, formatItem);
}
}
if (type == FragmentType.FormatItem)
throw new FormatException("A format item was opened with { but was never closed.");
fragmentEndIndex = numChars - 1;
string literalValue = format.Substring(startIndex);
if (foundEscapedDelimiter)
literalValue = ReplaceEscapes(literalValue);
return new Fragment(FragmentType.Literal, literalValue);
}
/// <summary>
/// Replaces escaped brackets, turning '{{' and '}}' into '{' and '}', respectively.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
static string ReplaceEscapes(string value)
{
return value.Replace("{{", "{").Replace("}}", "}");
}
private enum FragmentType
{
Literal,
FormatItem
}
private class Fragment
{
public Fragment(FragmentType type, string value)
{
Type = type;
Value = value;
}
public FragmentType Type
{
get;
private set;
}
/// <summary>
/// The literal value, or the name of the fragment, depending on fragment type.
/// </summary>
public string Value
{
get;
private set;
}
}
}
here is a simple method for any object:
using System.Text.RegularExpressions;
using System.ComponentModel;
public static string StringWithFormat(string format, object args)
{
Regex r = new Regex(#"\{([A-Za-z0-9_]+)\}");
MatchCollection m = r.Matches(format);
var properties = TypeDescriptor.GetProperties(args);
foreach (Match item in m)
{
try
{
string propertyName = item.Groups[1].Value;
format = format.Replace(item.Value, properties[propertyName].GetValue(args).ToString());
}
catch
{
throw new FormatException("The format string is not valid");
}
}
return format;
}
And here how to use it:
DateTime date = DateTime.Now;
string dateString = StringWithFormat("{Month}/{Day}/{Year}", date);
output : 2/27/2012
I implemented this is a simple class that duplicates the functionality of String.Format (except for when using classes). You can either use a dictionary or a type to define fields.
https://github.com/SergueiFedorov/NamedFormatString
C# 6.0 is adding this functionality right into the language spec, so NamedFormatString is for backwards compatibility.
I solved this in a slightly different way to the existing solutions.
It does the core of the named item replacement (not the reflection bit that some have done). It is extremely fast and simple...
This is my solution:
/// <summary>
/// Formats a string with named format items given a template dictionary of the items values to use.
/// </summary>
public class StringTemplateFormatter
{
private readonly IFormatProvider _formatProvider;
/// <summary>
/// Constructs the formatter with the specified <see cref="IFormatProvider"/>.
/// This is defaulted to <see cref="CultureInfo.CurrentCulture">CultureInfo.CurrentCulture</see> if none is provided.
/// </summary>
/// <param name="formatProvider"></param>
public StringTemplateFormatter(IFormatProvider formatProvider = null)
{
_formatProvider = formatProvider ?? CultureInfo.CurrentCulture;
}
/// <summary>
/// Formats a string with named format items given a template dictionary of the items values to use.
/// </summary>
/// <param name="text">The text template</param>
/// <param name="templateValues">The named values to use as replacements in the formatted string.</param>
/// <returns>The resultant text string with the template values replaced.</returns>
public string FormatTemplate(string text, Dictionary<string, object> templateValues)
{
var formattableString = text;
var values = new List<object>();
foreach (KeyValuePair<string, object> value in templateValues)
{
var index = values.Count;
formattableString = ReplaceFormattableItem(formattableString, value.Key, index);
values.Add(value.Value);
}
return String.Format(_formatProvider, formattableString, values.ToArray());
}
/// <summary>
/// Convert named string template item to numbered string template item that can be accepted by <see cref="string.Format(string,object[])">String.Format</see>
/// </summary>
/// <param name="formattableString">The string containing the named format item</param>
/// <param name="itemName">The name of the format item</param>
/// <param name="index">The index to use for the item value</param>
/// <returns>The formattable string with the named item substituted with the numbered format item.</returns>
private static string ReplaceFormattableItem(string formattableString, string itemName, int index)
{
return formattableString
.Replace("{" + itemName + "}", "{" + index + "}")
.Replace("{" + itemName + ",", "{" + index + ",")
.Replace("{" + itemName + ":", "{" + index + ":");
}
}
It is used in the following way:
[Test]
public void FormatTemplate_GivenANamedGuid_FormattedWithB_ShouldFormatCorrectly()
{
// Arrange
var template = "My guid {MyGuid:B} is awesome!";
var templateValues = new Dictionary<string, object> { { "MyGuid", new Guid("{A4D2A7F1-421C-4A1D-9CB2-9C2E70B05E19}") } };
var sut = new StringTemplateFormatter();
// Act
var result = sut.FormatTemplate(template, templateValues);
//Assert
Assert.That(result, Is.EqualTo("My guid {a4d2a7f1-421c-4a1d-9cb2-9c2e70b05e19} is awesome!"));
}
Hope someone finds this useful!
Even though the accepted answer gives some good examples, the .Inject as well as some of the Haack examples do not handle escaping. Many also rely heavily on Regex (slower), or DataBinder.Eval which is not available on .NET Core, and in some other environments.
With that in mind, I've written a simple state machine based parser that streams through characters, writing to a StringBuilder output, character by character. It is implemented as String extension method(s) and can take both a Dictionary<string, object> or object with parameters as input (using reflection).
It handles unlimited levels of {{{escaping}}} and throws FormatException when input contains unbalanced braces and/or other errors.
public static class StringExtension {
/// <summary>
/// Extension method that replaces keys in a string with the values of matching object properties.
/// </summary>
/// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
/// <param name="injectionObject">The object whose properties should be injected in the string</param>
/// <returns>A version of the formatString string with keys replaced by (formatted) key values.</returns>
public static string FormatWith(this string formatString, object injectionObject) {
return formatString.FormatWith(GetPropertiesDictionary(injectionObject));
}
/// <summary>
/// Extension method that replaces keys in a string with the values of matching dictionary entries.
/// </summary>
/// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
/// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param>
/// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns>
public static string FormatWith(this string formatString, IDictionary<string, object> dictionary) {
char openBraceChar = '{';
char closeBraceChar = '}';
return FormatWith(formatString, dictionary, openBraceChar, closeBraceChar);
}
/// <summary>
/// Extension method that replaces keys in a string with the values of matching dictionary entries.
/// </summary>
/// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
/// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param>
/// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns>
public static string FormatWith(this string formatString, IDictionary<string, object> dictionary, char openBraceChar, char closeBraceChar) {
string result = formatString;
if (dictionary == null || formatString == null)
return result;
// start the state machine!
// ballpark output string as two times the length of the input string for performance (avoids reallocating the buffer as often).
StringBuilder outputString = new StringBuilder(formatString.Length * 2);
StringBuilder currentKey = new StringBuilder();
bool insideBraces = false;
int index = 0;
while (index < formatString.Length) {
if (!insideBraces) {
// currently not inside a pair of braces in the format string
if (formatString[index] == openBraceChar) {
// check if the brace is escaped
if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) {
// add a brace to the output string
outputString.Append(openBraceChar);
// skip over braces
index += 2;
continue;
}
else {
// not an escaped brace, set state to inside brace
insideBraces = true;
index++;
continue;
}
}
else if (formatString[index] == closeBraceChar) {
// handle case where closing brace is encountered outside braces
if (index < formatString.Length - 1 && formatString[index + 1] == closeBraceChar) {
// this is an escaped closing brace, this is okay
// add a closing brace to the output string
outputString.Append(closeBraceChar);
// skip over braces
index += 2;
continue;
}
else {
// this is an unescaped closing brace outside of braces.
// throw a format exception
throw new FormatException($"Unmatched closing brace at position {index}");
}
}
else {
// the character has no special meaning, add it to the output string
outputString.Append(formatString[index]);
// move onto next character
index++;
continue;
}
}
else {
// currently inside a pair of braces in the format string
// found an opening brace
if (formatString[index] == openBraceChar) {
// check if the brace is escaped
if (index < formatString.Length - 1 && formatString[index + 1] == openBraceChar) {
// there are escaped braces within the key
// this is illegal, throw a format exception
throw new FormatException($"Illegal escaped opening braces within a parameter - index: {index}");
}
else {
// not an escaped brace, we have an unexpected opening brace within a pair of braces
throw new FormatException($"Unexpected opening brace inside a parameter - index: {index}");
}
}
else if (formatString[index] == closeBraceChar) {
// handle case where closing brace is encountered inside braces
// don't attempt to check for escaped braces here - always assume the first brace closes the braces
// since we cannot have escaped braces within parameters.
// set the state to be outside of any braces
insideBraces = false;
// jump over brace
index++;
// at this stage, a key is stored in current key that represents the text between the two braces
// do a lookup on this key
string key = currentKey.ToString();
// clear the stringbuilder for the key
currentKey.Clear();
object outObject;
if (!dictionary.TryGetValue(key, out outObject)) {
// the key was not found as a possible replacement, throw exception
throw new FormatException($"The parameter \"{key}\" was not present in the lookup dictionary");
}
// we now have the replacement value, add the value to the output string
outputString.Append(outObject);
// jump to next state
continue;
} // if }
else {
// character has no special meaning, add it to the current key
currentKey.Append(formatString[index]);
// move onto next character
index++;
continue;
} // else
} // if inside brace
} // while
// after the loop, if all braces were balanced, we should be outside all braces
// if we're not, the input string was misformatted.
if (insideBraces) {
throw new FormatException("The format string ended before the parameter was closed.");
}
return outputString.ToString();
}
/// <summary>
/// Creates a Dictionary from an objects properties, with the Key being the property's
/// name and the Value being the properties value (of type object)
/// </summary>
/// <param name="properties">An object who's properties will be used</param>
/// <returns>A <see cref="Dictionary"/> of property values </returns>
private static Dictionary<string, object> GetPropertiesDictionary(object properties) {
Dictionary<string, object> values = null;
if (properties != null) {
values = new Dictionary<string, object>();
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(properties);
foreach (PropertyDescriptor prop in props) {
values.Add(prop.Name, prop.GetValue(properties));
}
}
return values;
}
}
Ultimately, all the logic boils down into 10 main states - For when the state machine is outside a bracket and likewise inside a bracket, the next character is either an open brace, an escaped open brace, a closed brace, an escaped closed brace, or an ordinary character. Each of these conditions is handled individually as the loop progresses, adding characters to either an output StringBuffer or a key StringBuffer. When a parameter is closed, the value of the key StringBuffer is used to look up the parameter's value in the dictionary, which then gets pushed into the output StringBuffer. At the end, the value of the output StringBuffer is returned.
string language = "Python";
int numquotes = 2;
string output = language + " has "+ numquotes + " language types.";
Edit:
What I should have said was, "No, I don't believe what you want to do is supported by C#. This is as close as you are going to get."