Click here to Skip to main content
15,884,584 members
Articles / General Programming / Regular Expressions

A lightweight recursive text template data formatter

Rate me:
Please Sign up or sign in to vote.
4.92/5 (11 votes)
13 Mar 2013CPOL10 min read 41.4K   268   26  
Filling in text templates from a data source.
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Data;
using System.Reflection;
using System.Collections;
using System.Data.SqlClient;

namespace FillTextTemplate
{
	class Program
	{
		public class CustomerInfo
		{
			public string Name;
		}
		public class PartInfo
		{
			public string PartNumber;
			public string Description;
			public bool IsRestricted;
			public List<PartInfo> SimilarParts = null;
			public List<int> WarehouseIDs = null;
		}
		public class ItemInfo
		{
			public PartInfo Part;
			public decimal Quantity;
			public decimal UnitPrice;
			public decimal TotalPrice
			{
				get
				{
					return Quantity * UnitPrice;
				}
			}
		}

		public class OrderInfo
		{
			public CustomerInfo Customer = null;
			public int OrderNumber;
			public List<ItemInfo> Items = null;
			public decimal TotalPrice
			{
				get
				{
					if (Items == null) return 0M;
					decimal sum = 0.0M;
					foreach (var i in Items) sum += i.TotalPrice;
					return sum;
				}
			}
		}

		static void Main(string[] args)
		{

			PartInfo widget1 = new PartInfo()
			{
				PartNumber = "ABC-001",
				Description = "Widget #1",
				IsRestricted = true,
				WarehouseIDs = new List<int>() { 1, 2 }
			};
			PartInfo widget2 = new PartInfo()
			{
				PartNumber = "ABC-002",
				Description = "Widget #2",
				SimilarParts = new List<PartInfo>() { widget1 },
				WarehouseIDs = new List<int>() { 2, 3, 4 }
			};
			PartInfo widget3 = new PartInfo()
			{
				PartNumber = "ABC-003",
				Description = "Widget #3",
				SimilarParts = new List<PartInfo>() { widget1, widget2 }
			};
			OrderInfo order = new OrderInfo()
			{
				Customer = new CustomerInfo() { Name = "Michael Bray" },
				OrderNumber = 173123,
				Items = new List<ItemInfo>() { 
					new ItemInfo() { Part = widget1, Quantity = 2, UnitPrice = 30 },
					new ItemInfo() { Part = widget2, Quantity = 4.5M, UnitPrice = 10 },
					new ItemInfo() { Part = widget3, Quantity = 60, UnitPrice = 4.25M }
				}
			};

			string template, filled;

			template = "Hello @Customer.Name, your order #@OrderNumber has been fulfilled.  "
					+ "The items are:\r\n\r\n"
					+ "Part Number  Description           R  Quantity  Unit Price  Total Price  Similar Part Numbers  Warehouse IDs\r\n"
					+ "-----------  --------------------  -  --------  ----------  -----------  --------------------  -------------\r\n"
					+ "@?[[^ @Items.Count > 0 ^]][[^@Items[[#@Part.PartNumber{-12} @Part.Description{-20}  @?[[$ @Part.IsRestricted==True $]][[$Y$]][[$ $]]  @Quantity{8}{F2} @UnitPrice{11}{C} @TotalPrice{12}{C}  @Part.SimilarParts{-20}[[%@PartNumber,%]]  @Part.WarehouseIDs{-10}[[%@$,%]]\r\n#]]"
					+ "                                                            -----------\r\n"
					+ "                                              GRAND TOTAL: @TotalPrice{12}{C}\r\n"
					+ "^]][[^  NONE!!!  ^]]";
			filled = Demo.FillTemplate(template, order);


			template = "Parts List:\r\n"
				+ "Part Number    Description            Is Restricted\r\n"
				+ "-------------  ---------------------  -------------\r\n"
				+ "@$[[% @PartNumber{-12}  @Description{-20}  @?[[# @IsRestricted == True #]][[# YES #]][[# NO #]] \r\n%]]";
			filled = Demo.FillTemplate(template, new List<PartInfo>() { widget1, widget2, widget3 });
		}

	}

	public static class Demo
	{
		private static Regex rgxFillTemplate = new Regex(@"(?<VarName>@(\w+(\.\w+)*|\$))({(?<Width>-?\d+)})?({(?<Format>.+?)}|(((?<Open>\[{2}(?<CloseC>.))).*?(?<SubExpr-Open>\k<CloseC>\]{2})){1,3})?", RegexOptions.Compiled | RegexOptions.Singleline);
		private static Regex rgxConditional = new Regex(@"(?<VarName>@\??)(((?<Open>\[{2}(?<CloseC>.))).*?(?<SubExpr-Open>\k<CloseC>\]{2})){2,3}", RegexOptions.Compiled | RegexOptions.Singleline);
		public static string FillTemplate(string template, object ds)
		{
			StringBuilder sb = new StringBuilder(template);

			MatchCollection mc = rgxFillTemplate.Matches(template);
			IDataRecord dr = ds as IDataRecord;
			IDataReader drr = ds as IDataReader;
			foreach (Match m in mc)
			{
				object o = ds;
				string varName = m.Groups["VarName"].Value.TrimStart('@');
				string width = m.Groups["Width"].Success ? "," + m.Groups["Width"].Value : string.Empty;
				string format = m.Groups["Format"].Success ? ":" + m.Groups["Format"].Value : string.Empty;
				string varFormat = "{0" + width + format + "}";
				string varSubExpr = m.Groups["SubExpr"].Success ? m.Groups["SubExpr"].Value : null;
				string vv = string.Format(varFormat, string.Empty);

				if (dr != null)
				{
					try
					{
						if ((varName == "$") && (varSubExpr != null) && (drr != null))
						{
							List<string> vvs = new List<string>();
							while (drr.Read())
							{
								vvs.Add(FillTemplate(varSubExpr, dr));
							}
							vv = string.Format(varFormat, string.Join(string.Empty, vvs.ToArray()));
						}
						else
						{
							int vi = dr.GetOrdinal(varName);
							vv = string.Format(varFormat, dr.IsDBNull(vi) ? string.Empty : dr[vi]);
						}
					}
					catch (IndexOutOfRangeException) { }
				}
				else
				{
					string[] varPath = varName.Split('.');
					for (int vi = 0; vi < varPath.Length; vi++)
					{
						string vn = varPath[vi];
						if (vn == "$") break;

						Type t = o.GetType();
						var mems = t.GetMember(vn, BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
						object fo = null;
						foreach (var mi in mems)
						{
							if (mi != null)
							{
								if (mi is PropertyInfo)
								{
									PropertyInfo pi = mi as PropertyInfo;
									fo = pi.GetValue(o, null);
									break;
								}
								else if (mi is FieldInfo)
								{
									FieldInfo fi = mi as FieldInfo;
									fo = fi.GetValue(o);
									break;
								}
							}
						}
						o = fo;
						if (o == null) break;
					}
					if (o != null)
					{
						if (varSubExpr != null)
						{
							List<string> vvs = new List<string>();
							if (o is IEnumerable)
							{
								IEnumerable oe = o as IEnumerable;
								foreach (var oo in oe) vvs.Add(FillTemplate(varSubExpr, oo));
							}
							vv = string.Format(varFormat, string.Join(string.Empty, vvs.ToArray()));
						}
						else vv = string.Format(varFormat, o);
					}
				}
				sb = sb.Replace(m.Value, vv);
			}

			mc = rgxConditional.Matches(sb.ToString());
			foreach (Match m in mc)
			{
				int numPhrases = m.Groups["SubExpr"].Captures.Count;
				string conditional = m.Groups["SubExpr"].Captures[0].Value;
				string yesExpr = m.Groups["SubExpr"].Captures[1].Value;
				string noExpr = string.Empty;
				if (numPhrases == 3) noExpr = m.Groups["SubExpr"].Captures[2].Value;

				Match cm = Regex.Match(conditional, @"^\s*(?<Left>.*?)\s*(?<Op>\<=?|\>=?|==|!=)\s*(?<Right>.*?)\s*$");
				if (!cm.Success) sb = sb.Replace(m.Value, FillTemplate(noExpr, ds));
				else
				{
					bool success = true;
					int cv;

					string sLeft = cm.Groups["Left"].Value;
					string sRight = cm.Groups["Right"].Value;
					string sOp = cm.Groups["Op"].Value;

					decimal dLeft = 0, dRight = 0;
					success = success && decimal.TryParse(sLeft, out dLeft);
					success = success && decimal.TryParse(sRight, out dRight);
					if (success) cv = Comparer<decimal>.Default.Compare(dLeft, dRight);
					else cv = Comparer<string>.Default.Compare(sLeft, sRight);

					string repExpr = string.Empty;
					switch (sOp)
					{
						case "<": repExpr = (cv < 0) ? yesExpr : noExpr; break;
						case ">": repExpr = (cv > 0) ? yesExpr : noExpr; break;
						case "<=": repExpr = (cv <= 0) ? yesExpr : noExpr; break;
						case ">=": repExpr = (cv >= 0) ? yesExpr : noExpr; break;
						case "==": repExpr = (cv == 0) ? yesExpr : noExpr; break;
						case "!=": repExpr = (cv != 0) ? yesExpr : noExpr; break;
					}

					sb = sb.Replace(m.Value, FillTemplate(repExpr, ds));
				}

			}

			return sb.ToString();
		}

	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Presidio Network Solutions
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions