URL Slugify algorithm in C#? - c#

So I have searched and browsed through the slug tag on SO and only found two compelling solution:
Slugify and Character Transliteration in C#
How to convert super- or subscript to normal text in C#
Which are but partial solution to the problem. I could manually code this up myself but I'm surprised that there isn't already a solution out there yet.
So, is there a slugify alrogithm implementation in C# and/or .NET that properly address latin characters, unicode and various other language issues properly?

http://predicatet.blogspot.com/2009/04/improved-c-slug-generator-or-how-to.html
public static string GenerateSlug(this string phrase)
{
string str = phrase.RemoveAccent().ToLower();
// invalid chars
str = Regex.Replace(str, #"[^a-z0-9\s-]", "");
// convert multiple spaces into one space
str = Regex.Replace(str, #"\s+", " ").Trim();
// cut and trim
str = str.Substring(0, str.Length <= 45 ? str.Length : 45).Trim();
str = Regex.Replace(str, #"\s", "-"); // hyphens
return str;
}
public static string RemoveAccent(this string txt)
{
byte[] bytes = System.Text.Encoding.GetEncoding("Cyrillic").GetBytes(txt);
return System.Text.Encoding.ASCII.GetString(bytes);
}

Here you find a way to generate url slug in c#. This function remove all accents(Marcel's answer), replace spaces, remove invalid chars, trim dashes from end and replace double occurences of "-" or "_"
Code:
public static string ToUrlSlug(string value){
//First to lower case
value = value.ToLowerInvariant();
//Remove all accents
var bytes = Encoding.GetEncoding("Cyrillic").GetBytes(value);
value = Encoding.ASCII.GetString(bytes);
//Replace spaces
value = Regex.Replace(value, #"\s", "-", RegexOptions.Compiled);
//Remove invalid chars
value = Regex.Replace(value, #"[^a-z0-9\s-_]", "",RegexOptions.Compiled);
//Trim dashes from end
value = value.Trim('-', '_');
//Replace double occurences of - or _
value = Regex.Replace(value, #"([-_]){2,}", "$1", RegexOptions.Compiled);
return value ;
}

Here is my rendition, based Joan's and Marcel's answers. The changes I made are as follows:
Use a widely accepted method to remove accents.
Explicit Regex caching for modest speed improvements.
More word separators recognized and normalized to hyphens.
Here is the code:
public class UrlSlugger
{
// white space, em-dash, en-dash, underscore
static readonly Regex WordDelimiters = new Regex(#"[\s—–_]", RegexOptions.Compiled);
// characters that are not valid
static readonly Regex InvalidChars = new Regex(#"[^a-z0-9\-]", RegexOptions.Compiled);
// multiple hyphens
static readonly Regex MultipleHyphens = new Regex(#"-{2,}", RegexOptions.Compiled);
public static string ToUrlSlug(string value)
{
// convert to lower case
value = value.ToLowerInvariant();
// remove diacritics (accents)
value = RemoveDiacritics(value);
// ensure all word delimiters are hyphens
value = WordDelimiters.Replace(value, "-");
// strip out invalid characters
value = InvalidChars.Replace(value, "");
// replace multiple hyphens (-) with a single hyphen
value = MultipleHyphens.Replace(value, "-");
// trim hyphens (-) from ends
return value.Trim('-');
}
/// See: http://www.siao2.com/2007/05/14/2629747.aspx
private static string RemoveDiacritics(string stIn)
{
string stFormD = stIn.Normalize(NormalizationForm.FormD);
StringBuilder sb = new StringBuilder();
for (int ich = 0; ich < stFormD.Length; ich++)
{
UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory(stFormD[ich]);
if (uc != UnicodeCategory.NonSpacingMark)
{
sb.Append(stFormD[ich]);
}
}
return (sb.ToString().Normalize(NormalizationForm.FormC));
}
}
This still does not solve the non-latin character issue. A completely alternative solution would be to use Uri.EscapeDataString to convert the the string its hex representation:
string original = "测试公司";
// %E6%B5%8B%E8%AF%95%E5%85%AC%E5%8F%B8
string converted = Uri.EscapeDataString(original);
Then use the data to generate a hyperlink:
<a href="http://www.example.com/100/%E6%B5%8B%E8%AF%95%E5%85%AC%E5%8F%B8">
测试公司
</a>
Many browsers will display Chinese characters in the address bar (see below), but based on my limited testing, it is not completely supported.
NOTE: In order for Uri.EscapeDataString to work this way, iriParsing must be enabled.
EDIT
For those looking to generate URL Slugs in C#, I recommend checking out this related question:
How does Stack Overflow generate its SEO-friendly URLs?
It is what I ended up using for my project.

One problem I've had with slugification (new word!) is collisions. If I have a blog post, for instance, called "Stack-Overflow" and one called "Stack Overflow", the slugs of those two titles are the same. Therefore, my slug generator usually has to involve the database in some way. This might be why you don't see more generic solutions out there.

Here is my shot at it. It supports:
removal of diacritics (so we don't just remove "invalid" characters)
max length for the result (or before removal of diacritics - "early truncate")
custom separator between normalized chunks
the result can be forced to uppercase or lowercase
configurable list of supported unicode categories
configurable list of ranges of allowed characters
supports framework 2.0
Code:
/// <summary>
/// Defines a set of utilities for creating slug urls.
/// </summary>
public static class Slug
{
/// <summary>
/// Creates a slug from the specified text.
/// </summary>
/// <param name="text">The text. If null if specified, null will be returned.</param>
/// <returns>
/// A slugged text.
/// </returns>
public static string Create(string text)
{
return Create(text, (SlugOptions)null);
}
/// <summary>
/// Creates a slug from the specified text.
/// </summary>
/// <param name="text">The text. If null if specified, null will be returned.</param>
/// <param name="options">The options. May be null.</param>
/// <returns>A slugged text.</returns>
public static string Create(string text, SlugOptions options)
{
if (text == null)
return null;
if (options == null)
{
options = new SlugOptions();
}
string normalised;
if (options.EarlyTruncate && options.MaximumLength > 0 && text.Length > options.MaximumLength)
{
normalised = text.Substring(0, options.MaximumLength).Normalize(NormalizationForm.FormD);
}
else
{
normalised = text.Normalize(NormalizationForm.FormD);
}
int max = options.MaximumLength > 0 ? Math.Min(normalised.Length, options.MaximumLength) : normalised.Length;
StringBuilder sb = new StringBuilder(max);
for (int i = 0; i < normalised.Length; i++)
{
char c = normalised[i];
UnicodeCategory uc = char.GetUnicodeCategory(c);
if (options.AllowedUnicodeCategories.Contains(uc) && options.IsAllowed(c))
{
switch (uc)
{
case UnicodeCategory.UppercaseLetter:
if (options.ToLower)
{
c = options.Culture != null ? char.ToLower(c, options.Culture) : char.ToLowerInvariant(c);
}
sb.Append(options.Replace(c));
break;
case UnicodeCategory.LowercaseLetter:
if (options.ToUpper)
{
c = options.Culture != null ? char.ToUpper(c, options.Culture) : char.ToUpperInvariant(c);
}
sb.Append(options.Replace(c));
break;
default:
sb.Append(options.Replace(c));
break;
}
}
else if (uc == UnicodeCategory.NonSpacingMark)
{
// don't add a separator
}
else
{
if (options.Separator != null && !EndsWith(sb, options.Separator))
{
sb.Append(options.Separator);
}
}
if (options.MaximumLength > 0 && sb.Length >= options.MaximumLength)
break;
}
string result = sb.ToString();
if (options.MaximumLength > 0 && result.Length > options.MaximumLength)
{
result = result.Substring(0, options.MaximumLength);
}
if (!options.CanEndWithSeparator && options.Separator != null && result.EndsWith(options.Separator))
{
result = result.Substring(0, result.Length - options.Separator.Length);
}
return result.Normalize(NormalizationForm.FormC);
}
private static bool EndsWith(StringBuilder sb, string text)
{
if (sb.Length < text.Length)
return false;
for (int i = 0; i < text.Length; i++)
{
if (sb[sb.Length - 1 - i] != text[text.Length - 1 - i])
return false;
}
return true;
}
}
/// <summary>
/// Defines options for the Slug utility class.
/// </summary>
public class SlugOptions
{
/// <summary>
/// Defines the default maximum length. Currently equal to 80.
/// </summary>
public const int DefaultMaximumLength = 80;
/// <summary>
/// Defines the default separator. Currently equal to "-".
/// </summary>
public const string DefaultSeparator = "-";
private bool _toLower;
private bool _toUpper;
/// <summary>
/// Initializes a new instance of the <see cref="SlugOptions"/> class.
/// </summary>
public SlugOptions()
{
MaximumLength = DefaultMaximumLength;
Separator = DefaultSeparator;
AllowedUnicodeCategories = new List<UnicodeCategory>();
AllowedUnicodeCategories.Add(UnicodeCategory.UppercaseLetter);
AllowedUnicodeCategories.Add(UnicodeCategory.LowercaseLetter);
AllowedUnicodeCategories.Add(UnicodeCategory.DecimalDigitNumber);
AllowedRanges = new List<KeyValuePair<short, short>>();
AllowedRanges.Add(new KeyValuePair<short, short>((short)'a', (short)'z'));
AllowedRanges.Add(new KeyValuePair<short, short>((short)'A', (short)'Z'));
AllowedRanges.Add(new KeyValuePair<short, short>((short)'0', (short)'9'));
}
/// <summary>
/// Gets the allowed unicode categories list.
/// </summary>
/// <value>
/// The allowed unicode categories list.
/// </value>
public virtual IList<UnicodeCategory> AllowedUnicodeCategories { get; private set; }
/// <summary>
/// Gets the allowed ranges list.
/// </summary>
/// <value>
/// The allowed ranges list.
/// </value>
public virtual IList<KeyValuePair<short, short>> AllowedRanges { get; private set; }
/// <summary>
/// Gets or sets the maximum length.
/// </summary>
/// <value>
/// The maximum length.
/// </value>
public virtual int MaximumLength { get; set; }
/// <summary>
/// Gets or sets the separator.
/// </summary>
/// <value>
/// The separator.
/// </value>
public virtual string Separator { get; set; }
/// <summary>
/// Gets or sets the culture for case conversion.
/// </summary>
/// <value>
/// The culture.
/// </value>
public virtual CultureInfo Culture { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the string can end with a separator string.
/// </summary>
/// <value>
/// <c>true</c> if the string can end with a separator string; otherwise, <c>false</c>.
/// </value>
public virtual bool CanEndWithSeparator { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the string is truncated before normalization.
/// </summary>
/// <value>
/// <c>true</c> if the string is truncated before normalization; otherwise, <c>false</c>.
/// </value>
public virtual bool EarlyTruncate { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to lowercase the resulting string.
/// </summary>
/// <value>
/// <c>true</c> if the resulting string must be lowercased; otherwise, <c>false</c>.
/// </value>
public virtual bool ToLower
{
get
{
return _toLower;
}
set
{
_toLower = value;
if (_toLower)
{
_toUpper = false;
}
}
}
/// <summary>
/// Gets or sets a value indicating whether to uppercase the resulting string.
/// </summary>
/// <value>
/// <c>true</c> if the resulting string must be uppercased; otherwise, <c>false</c>.
/// </value>
public virtual bool ToUpper
{
get
{
return _toUpper;
}
set
{
_toUpper = value;
if (_toUpper)
{
_toLower = false;
}
}
}
/// <summary>
/// Determines whether the specified character is allowed.
/// </summary>
/// <param name="character">The character.</param>
/// <returns>true if the character is allowed; false otherwise.</returns>
public virtual bool IsAllowed(char character)
{
foreach (var p in AllowedRanges)
{
if (character >= p.Key && character <= p.Value)
return true;
}
return false;
}
/// <summary>
/// Replaces the specified character by a given string.
/// </summary>
/// <param name="character">The character to replace.</param>
/// <returns>a string.</returns>
public virtual string Replace(char character)
{
return character.ToString();
}
}

Related

Is it possible to have a conditional Required DataAnnotation for validation using Blazor/EditForm? [duplicate]

I have a class like this:
public class Document
{
public int DocumentType{get;set;}
[Required]
public string Name{get;set;}
[Required]
public string Name2{get;set;}
}
Now if I put a [Required] data annotation on the Name and Name2 properties, then everything is ok and if Name or Name2 are empty, validation will throw an error.
But I want Name field only to be required if DocumentType is equal to 1
and Name2 only required if DocumentType is equal to 2 .
public class Document
{
public int DocumentType{get;set;}
[Required(Expression<Func<object, bool>>)]
public string Name{get;set;}
[Required(Expression<Func<object, bool>>)]
public string Name2{get;set;}
}
but I know I can't, it causes an error. What should I do for this requirement?
RequiredIf validation attribute
I've written a RequiredIfAttribute that requires a particular property value when a different property has a certain value (what you require) or when a different property has anything but a specific value.
This is the code that may help:
/// <summary>
/// Provides conditional validation based on related property value.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class RequiredIfAttribute : ValidationAttribute
{
#region Properties
/// <summary>
/// Gets or sets the other property name that will be used during validation.
/// </summary>
/// <value>
/// The other property name.
/// </value>
public string OtherProperty { get; private set; }
/// <summary>
/// Gets or sets the display name of the other property.
/// </summary>
/// <value>
/// The display name of the other property.
/// </value>
public string OtherPropertyDisplayName { get; set; }
/// <summary>
/// Gets or sets the other property value that will be relevant for validation.
/// </summary>
/// <value>
/// The other property value.
/// </value>
public object OtherPropertyValue { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether other property's value should match or differ from provided other property's value (default is <c>false</c>).
/// </summary>
/// <value>
/// <c>true</c> if other property's value validation should be inverted; otherwise, <c>false</c>.
/// </value>
/// <remarks>
/// How this works
/// - true: validated property is required when other property doesn't equal provided value
/// - false: validated property is required when other property matches provided value
/// </remarks>
public bool IsInverted { get; set; }
/// <summary>
/// Gets a value that indicates whether the attribute requires validation context.
/// </summary>
/// <returns><c>true</c> if the attribute requires validation context; otherwise, <c>false</c>.</returns>
public override bool RequiresValidationContext
{
get { return true; }
}
#endregion
#region Constructor
/// <summary>
/// Initializes a new instance of the <see cref="RequiredIfAttribute"/> class.
/// </summary>
/// <param name="otherProperty">The other property.</param>
/// <param name="otherPropertyValue">The other property value.</param>
public RequiredIfAttribute(string otherProperty, object otherPropertyValue)
: base("'{0}' is required because '{1}' has a value {3}'{2}'.")
{
this.OtherProperty = otherProperty;
this.OtherPropertyValue = otherPropertyValue;
this.IsInverted = false;
}
#endregion
/// <summary>
/// Applies formatting to an error message, based on the data field where the error occurred.
/// </summary>
/// <param name="name">The name to include in the formatted message.</param>
/// <returns>
/// An instance of the formatted error message.
/// </returns>
public override string FormatErrorMessage(string name)
{
return string.Format(
CultureInfo.CurrentCulture,
base.ErrorMessageString,
name,
this.OtherPropertyDisplayName ?? this.OtherProperty,
this.OtherPropertyValue,
this.IsInverted ? "other than " : "of ");
}
/// <summary>
/// Validates the specified value with respect to the current validation attribute.
/// </summary>
/// <param name="value">The value to validate.</param>
/// <param name="validationContext">The context information about the validation operation.</param>
/// <returns>
/// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult" /> class.
/// </returns>
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException("validationContext");
}
PropertyInfo otherProperty = validationContext.ObjectType.GetProperty(this.OtherProperty);
if (otherProperty == null)
{
return new ValidationResult(
string.Format(CultureInfo.CurrentCulture, "Could not find a property named '{0}'.", this.OtherProperty));
}
object otherValue = otherProperty.GetValue(validationContext.ObjectInstance);
// check if this value is actually required and validate it
if (!this.IsInverted && object.Equals(otherValue, this.OtherPropertyValue) ||
this.IsInverted && !object.Equals(otherValue, this.OtherPropertyValue))
{
if (value == null)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
// additional check for strings so they're not empty
string val = value as string;
if (val != null && val.Trim().Length == 0)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
}
return ValidationResult.Success;
}
}
Conditionally required property using data annotations
[RequiredIf(dependent Property name, dependent Property value)]
e.g.
[RequiredIf("Country", "Ethiopia")]
public string POBox{get;set;}
// POBox is required in Ethiopia
public string Country{get;set;}
[RequiredIf("destination", "US")]
public string State{get;set;}
// State is required in US
public string destination{get;set;}
public class RequiredIfAttribute : ValidationAttribute
{
RequiredAttribute _innerAttribute = new RequiredAttribute();
public string _dependentProperty { get; set; }
public object _targetValue { get; set; }
public RequiredIfAttribute(string dependentProperty, object targetValue)
{
this._dependentProperty = dependentProperty;
this._targetValue = targetValue;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var field = validationContext.ObjectType.GetProperty(_dependentProperty);
if (field != null)
{
var dependentValue = field.GetValue(validationContext.ObjectInstance, null);
if ((dependentValue == null && _targetValue == null) || (dependentValue.Equals(_targetValue)))
{
if (!_innerAttribute.IsValid(value))
{
string name = validationContext.DisplayName;
string specificErrorMessage = ErrorMessage;
if (specificErrorMessage.Length < 1)
specificErrorMessage = $"{name} is required.";
return new ValidationResult(specificErrorMessage, new[] { validationContext.MemberName });
}
}
return ValidationResult.Success;
}
else
{
return new ValidationResult(FormatErrorMessage(_dependentProperty));
}
}
}
Out of the box I think this is still not possible.
But I found this promising article about Mvc.ValidationToolkit (also here, unfortunately this is only alpha, but you probably could also just extract the method(s) you need from this code and integrate it on your own), it contains the nice sounding attribute RequiredIf which seems to match exactly your cause:
you download the project from the linked zip and build it
get the built dll from your build folder and reference it in the project you are using
unfortunately this seems to require reference to MVC, too (easiest way to have that is starting an MVC-Project in VS or install-package Microsoft.AspNet.Mvc)
in the files where you want to use it, you add using Mvc.ValidationToolkit;
then you are able to write things like [RequiredIf("DocumentType", 2)] or [RequiredIf("DocumentType", 1)], so objects are valid if neither name or name2 are supplied as long as DocumentType is not equal to 1 or 2
Check out Fluent Validation
https://www.nuget.org/packages/FluentValidation/
Project Description
A small validation library for .NET that uses a fluent interface and lambda expressions for building validation rules for your business objects.
https://github.com/JeremySkinner/FluentValidation
I have always used implemented IValidatableObject from System.ComponentModel.DataAnnotations;
Example below
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (this.SendInAppNotification)
{
if (string.IsNullOrEmpty(this.NotificationTitle) || string.IsNullOrWhiteSpace(this.NotificationTitle))
{
yield return new ValidationResult(
$"Notification Title is required",
new[] { nameof(this.NotificationTitle) });
}
}
check out the ExpressiveAnnotations .net library
Git reference
It has 'RequiredIf' and 'AssertThat' validation attributes
Check out MVC Foolproof validation. It has data annotation in model like RequiredIf (dependent Property, dependent value) if I remember correctly. You can download Foolproof from:
Visual Studio(2017) -> Tools -> Nuget Package Manager -> Manage Nuget Packages for Solution. Reference mvcfoolproof.unobtrusive.min.js in addition to the jquery files.
I solved this by extending the RequiredAttribute class, borrowing some logic from the CompareAttribute and Robert's excellent solution:
/// <summary>
/// Provides conditional <see cref="RequiredAttribute"/>
/// validation based on related property value.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class RequiredIfAttribute : RequiredAttribute
{
/// <summary>
/// Gets or sets a value indicating whether other property's value should
/// match or differ from provided other property's value (default is <c>false</c>).
/// </summary>
public bool IsInverted { get; set; } = false;
/// <summary>
/// Gets or sets the other property name that will be used during validation.
/// </summary>
/// <value>
/// The other property name.
/// </value>
public string OtherProperty { get; private set; }
/// <summary>
/// Gets or sets the other property value that will be relevant for validation.
/// </summary>
/// <value>
/// The other property value.
/// </value>
public object OtherPropertyValue { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="RequiredIfAttribute"/> class.
/// </summary>
/// <param name="otherProperty">The other property.</param>
/// <param name="otherPropertyValue">The other property value.</param>
public RequiredIfAttribute(string otherProperty, object otherPropertyValue)
: base()
{
OtherProperty = otherProperty;
OtherPropertyValue = otherPropertyValue;
}
protected override ValidationResult IsValid(
object value,
ValidationContext validationContext)
{
PropertyInfo otherPropertyInfo = validationContext
.ObjectType.GetProperty(OtherProperty);
if (otherPropertyInfo == null)
{
return new ValidationResult(
string.Format(
CultureInfo.CurrentCulture,
"Could not find a property named {0}.",
validationContext.ObjectType, OtherProperty));
}
// Determine whether to run [Required] validation
object actualOtherPropertyValue = otherPropertyInfo
.GetValue(validationContext.ObjectInstance, null);
if (!IsInverted && Equals(actualOtherPropertyValue, OtherPropertyValue) ||
IsInverted && !Equals(actualOtherPropertyValue, OtherPropertyValue))
{
return base.IsValid(value, validationContext);
}
return default;
}
}
Example usage:
public class Model {
public bool Subscribe { get; set; }
[RequiredIf(nameof(Subscribe), true)]
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
}
This way, you get all the standard Required validation features.
N.B.: I am using .NET 5, but I tried to remove language features added in c# 9.0 for wider compatibility.
I wrote a simple custom validation attribute that it's very readable.
using System;
using System.ComponentModel.DataAnnotations;
namespace some.namespace
{
public class RequiredIfAttribute : ValidationAttribute
{
public string PropertyName { get; set; }
public object Value { get; set; }
public RequiredIfAttribute(string propertyName, object value = null, string errorMessage = "")
{
PropertyName = propertyName;
Value = value;
ErrorMessage = errorMessage;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (PropertyName == null || PropertyName.ToString() == "")
{
throw new Exception("RequiredIf: you have to indicate the name of the property to use in the validation");
}
var propertyValue = GetPropertyValue(validationContext);
if (HasPropertyValue(propertyValue) && (value == null || value.ToString() == ""))
{
return new ValidationResult(ErrorMessage);
}
else
{
return ValidationResult.Success;
}
}
private object GetPropertyValue(ValidationContext validationContext)
{
var instance = validationContext.ObjectInstance;
var type = instance.GetType();
return type.GetProperty(PropertyName).GetValue(instance);
}
private bool HasPropertyValue(object propertyValue)
{
if (Value != null)
{
return propertyValue != null && propertyValue.ToString() == Value.ToString();
}
else
{
return propertyValue != null && propertyValue.ToString() != "";
}
}
}
}
You can use it like this
public class Document
{
public int DocumentType{get;set;}
[RequiredIf("DocumentType", "1", ErrorMessage = "The field is required.")]
public string Name{get;set;}
[RequiredIf("DocumentType", "2", ErrorMessage = "The field is required.")]
public string Name2{get;set;}
}
I know this question is from a long time ago but someone asked in the comments section of Robert's answer how to use unobtrusive as part of the solution.
I wanted client side validation as well so I'm sharing my revised code to Robert's original code. It's essentially the same code except it implements IClientModelValidator and has an additional AddValidation method. The client validation still respects the IsInverted property.
Implement IClientModelValidator
public sealed class RequiredIfAttribute : ValidationAttribute, IClientModelValidator
New AddValidation method
public void AddValidation(ClientModelValidationContext context)
{
var viewContext = context.ActionContext as ViewContext;
var modelType = context.ModelMetadata.ContainerType;
var instance = viewContext?.ViewData.Model;
var model = instance?.GetType().Name == modelType.Name
? instance
: instance?.GetType()?.GetProperties().First(x => x.PropertyType.Name == modelType.Name)
.GetValue(instance, null);
object otherValue = modelType.GetProperty(this.OtherProperty)?.GetValue(model, null);
object value = modelType.GetProperty(context.ModelMetadata.Name)?.GetValue(model, null);
string displayName = context.ModelMetadata.DisplayName ?? context.ModelMetadata.Name;
string errorMessage = null;
// check if this value is actually required and validate it
if (!this.IsInverted && object.Equals(otherValue, this.OtherPropertyValue) ||
this.IsInverted && !object.Equals(otherValue, this.OtherPropertyValue))
{
if (value == null)
{
errorMessage = this.FormatErrorMessage(displayName);
}
// additional check for strings so they're not empty
string val = value as string;
if (val != null && val.Trim().Length == 0)
{
errorMessage = this.FormatErrorMessage(displayName);
}
}
if (!string.IsNullOrWhiteSpace(errorMessage))
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-required", errorMessage);
}
}
Full Code
/// <summary>
/// Provides conditional validation based on related property value.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class RequiredIfAttribute : ValidationAttribute, IClientModelValidator
{
#region Properties
/// <summary>
/// Gets or sets the other property name that will be used during validation.
/// </summary>
/// <value>
/// The other property name.
/// </value>
public string OtherProperty { get; private set; }
/// <summary>
/// Gets or sets the display name of the other property.
/// </summary>
/// <value>
/// The display name of the other property.
/// </value>
public string OtherPropertyDisplayName { get; set; }
/// <summary>
/// Gets or sets the other property value that will be relevant for validation.
/// </summary>
/// <value>
/// The other property value.
/// </value>
public object OtherPropertyValue { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether other property's value should match or differ from provided other property's value (default is <c>false</c>).
/// </summary>
/// <value>
/// <c>true</c> if other property's value validation should be inverted; otherwise, <c>false</c>.
/// </value>
/// <remarks>
/// How this works
/// - true: validated property is required when other property doesn't equal provided value
/// - false: validated property is required when other property matches provided value
/// </remarks>
public bool IsInverted { get; set; }
/// <summary>
/// Gets a value that indicates whether the attribute requires validation context.
/// </summary>
/// <returns><c>true</c> if the attribute requires validation context; otherwise, <c>false</c>.</returns>
public override bool RequiresValidationContext
{
get { return true; }
}
#endregion
#region Constructor
/// <summary>
/// Initializes a new instance of the <see cref="RequiredIfAttribute"/> class.
/// </summary>
/// <param name="otherProperty">The other property.</param>
/// <param name="otherPropertyValue">The other property value.</param>
public RequiredIfAttribute(string otherProperty, object otherPropertyValue)
: base("'{0}' is required because '{1}' has a value {3}'{2}'.")
{
this.OtherProperty = otherProperty;
this.OtherPropertyValue = otherPropertyValue;
this.IsInverted = false;
}
#endregion
public void AddValidation(ClientModelValidationContext context)
{
var viewContext = context.ActionContext as ViewContext;
var modelType = context.ModelMetadata.ContainerType;
var instance = viewContext?.ViewData.Model;
var model = instance?.GetType().Name == modelType.Name
? instance
: instance?.GetType()?.GetProperties().First(x => x.PropertyType.Name == modelType.Name)
.GetValue(instance, null);
object otherValue = modelType.GetProperty(this.OtherProperty)?.GetValue(model, null);
object value = modelType.GetProperty(context.ModelMetadata.Name)?.GetValue(model, null);
string displayName = context.ModelMetadata.DisplayName ?? context.ModelMetadata.Name;
string errorMessage = null;
// check if this value is actually required and validate it
if (!this.IsInverted && object.Equals(otherValue, this.OtherPropertyValue) ||
this.IsInverted && !object.Equals(otherValue, this.OtherPropertyValue))
{
if (value == null)
{
errorMessage = this.FormatErrorMessage(displayName);
}
// additional check for strings so they're not empty
string val = value as string;
if (val != null && val.Trim().Length == 0)
{
errorMessage = this.FormatErrorMessage(displayName);
}
}
if (!string.IsNullOrWhiteSpace(errorMessage))
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-required", errorMessage);
}
}
/// <summary>
/// Applies formatting to an error message, based on the data field where the error occurred.
/// </summary>
/// <param name="name">The name to include in the formatted message.</param>
/// <returns>
/// An instance of the formatted error message.
/// </returns>
public override string FormatErrorMessage(string name)
{
return string.Format(
CultureInfo.CurrentCulture,
base.ErrorMessageString,
name,
this.OtherPropertyDisplayName ?? this.OtherProperty,
this.OtherPropertyValue,
this.IsInverted ? "other than " : "of ");
}
/// <summary>
/// Validates the specified value with respect to the current validation attribute.
/// </summary>
/// <param name="value">The value to validate.</param>
/// <param name="validationContext">The context information about the validation operation.</param>
/// <returns>
/// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult" /> class.
/// </returns>
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException("validationContext");
}
PropertyInfo otherProperty = validationContext.ObjectType.GetProperty(this.OtherProperty);
if (otherProperty == null)
{
return new ValidationResult(
string.Format(CultureInfo.CurrentCulture, "Could not find a property named '{0}'.", this.OtherProperty));
}
object otherValue = otherProperty.GetValue(validationContext.ObjectInstance);
// check if this value is actually required and validate it
if (!this.IsInverted && object.Equals(otherValue, this.OtherPropertyValue) ||
this.IsInverted && !object.Equals(otherValue, this.OtherPropertyValue))
{
if (value == null)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
// additional check for strings so they're not empty
string val = value as string;
if (val != null && val.Trim().Length == 0)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
}
return ValidationResult.Success;
}
}
This should just work, provided you have included jquery.js, jquery.validate.js and jquery.validate.unobtrusive.js script files (in that order) to your layout or razor view.
I can't give you exactly what you're asking for, but have you considered something like the following?
public abstract class Document // or interface, whichever is appropriate for you
{
//some non-validted common properties
}
public class ValidatedDocument : Document
{
[Required]
public string Name {get;set;}
}
public class AnotherValidatedDocument : Document
{
[Required]
public string Name {get;set;}
//I would suggest finding a descriptive name for this instead of Name2,
//Name2 doesn't make it clear what it's for
public string Name2 {get;set;}
}
public class NonValidatedDocument : Document
{
public string Name {get;set;}
}
//Etc...
Justification being the int DocumentType variable. You could replace this with using concrete subclass types for each "type" of document you need to deal with. Doing this gives you much better control of your property annotations.
It also appears that only some of your properties are needed in different situations, which could be a sign that your document class is trying to do too much, and supports the suggestion above.

Unable to assign to Hashtable item in C# class consumed from VBA

I have written a C# class (KvpHash) for consumption in VBA which provides additional useful functionality around a Hashtable. In my VBA code I have an extensive Rubberduck test suite for the KvpHash class that shows all functions of the class work as expected except for the fact that I cannot change the value of an item.
From VBA I get the error message
424 'Object required'
In the C# class the interface code is
dynamic this[dynamic Key] { get; set; }
and the implementation is
public dynamic this[dynamic Key]
{
get
{
return MyKvpHash[Key];
}
set
{
MyKvpHash[Key] = value;
}
}
where MyKvpHash is defined as
private Hashtable MyKvpHash = new Hashtable();
If I add the mscorelib reference to VBA I can create a Hashtable directly in VBA where it is fully possible to change the value of an item in the Hash Table.
I'd appreciate pointers as to what I am doing wrong in the C# code which causes the object required error.
Edited: to add example VBA code
Using a native HashTable
Public Sub TestHashtable()
' requires reference to mscorlib.dll
Dim myHt As Hashtable
Set myHt = New Hashtable
myHt.Add 5, "Hello5"
myHt.Add 10, "Hello10"
Debug.Print myHt.Item(10)
Debug.Print myHt.Item(5)
' the next line works as expected
myHt.Item(10) = "A new world"
Debug.Print myHt.Item(10)
End Sub
Gives the output
Hello10
Hello5
A new world
Using my KvpHash class ( a wrapper for HashTable)
Public Sub TestKvpHash()
Dim myHt As VBAExtensions.KvpHash
' KvpHash is a C# wrapper for a System.Collections.HashTable
Set myHt = New KvpHash
myHt.AddByKey 5, "Hello5"
myHt.AddByKey 10, "Hello10"
Debug.Print myHt.Item(10)
Debug.Print myHt.Item(5)
' The next line produces error 424
myHt.Item(10) = "A new world"
Debug.Print myHt.Item(10)
End Sub
Gives the output
Hello10
Hello5
and then stops with the 424 error.
Edited to add the full C# code as requested.
Seems there is no file hosting and I don't have another means of providing a link so I'v inserted the relevant code below. The code was originally based on a Dictionary but I updated it to Hashtable when I first found I couldn't assign to an item. That switch didn't change the behaviour of my code. Please be aware that I'm not a professional programmer and that the supplied code is essentially my first foray into C#. Typically I write Word VBA macros for my own consumption.
// VBAExtensions
//
// C# Library module for VBA
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Linq;
namespace VBAExtensions
{
/// <summary>
/// Enum for accessing the kvphash structure returned by Method Cohorts
/// </summary>
public enum CohortType
{
/// <summary>1 = the keys in A plus keys in B that are not shared</summary>
KeysInAAndOnlyB = 1,
/// <summary>2 = the Keys from B in A where B has a different value to A</summary>
KeysInAandBWithDifferentValues,
/// <summary>3 = the keys that are only in A and only in B</summary>
KeysNotInAandB,
/// <summary>4 = the keys that are inA and B </summary>
KeysInAandB,
/// <summary>5 = the keys in A only </summary>
KeysInAOnly,
/// <summary>6 = the keys in B only</summary>
KeysInBOnly
}
/// <summary>
/// KvpHash is a C# class for VBA which implements a Key/Value HashTable
/// The object is a morer flexible version of the Scripting.Dictionary
/// </summary>
[Guid("30F9294B-11B4-4D91-9D7C-7FF02ADB3F11")]
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IKvpHash
{
/// <summary>
/// Returns/Sets the "Value" specified by "Key" (i) of a Key/Value Pair
/// </summary>
/// <param name="Key"></param>
/// <returns>Type used in Set statement (C# dynamic)</returns>
dynamic this[dynamic Key] { get; set; }
/// <summary>
/// Adds "Value" to the KvpHash using an integer (VBA Long) Key.
/// The integer key is based on the first available integer greater than or
/// equal to the Count of the KvpHash
/// </summary>
/// <param name="Value"></param>
void AddByIndex(dynamic Value);
/// <summary>
/// Populates this KvpHash using AddByIndex for each character in the string
/// </summary>
/// <param name="this_string"></param>
void AddByIndexAsChars(string this_string);
/// <summary>
/// Pupulates this KvpHash using AddByIndex for each substring in this_string delineated by this_seperator
/// </summary>
/// <param name="this_string"></param>
/// <param name="this_seperator"></param>
void AddByIndexAsSubStr(string this_string, string this_seperator = ",");
/// <summary>
/// Pupulates a KvpHash using AddByIndex for each array item
/// </summary>
/// <param name="this_array"></param>
void AddByIndexFromArray(dynamic this_array);
/// <summary>
/// Adds "Value" to the KvpHash with a key pf "Key"
/// </summary>
/// <param name="Key"></param>
/// <param name="Value"></param>
void AddByKey(dynamic Key, dynamic Value);
/// <summary>
/// Groups the keys of the two KvpHash
/// </summary>
/// <param name="ArgKvpHash"></param>
/// <returns>An array of 6 KvpHash
/// keys in a {1,2,3,4,5,6}
/// keys in b {1,2,3,6,7,8}
/// 1 = the keys in A plus keys in B that are not shared {1,2,3( from A),4,5,6,7,8}
/// 2 = the Keys from B in A where B has a different value to A {3( from B) if value is different}
/// 3 = the keys that are only in A and only in B {4,5,7,8}
/// 4 = the keys that are in A and B {1,2,3,6}
/// 5 = the keys in A only {4,5}
/// 6 = the keys in B only {7,8}
/// </returns>
KvpHash Cohorts(KvpHash ArgKvpHash);
/// <summary>
/// The number of key/vaue pairs in the KvpHash
/// </summary>
/// <returns>Long</returns>
int Count();
///// <summary>
///// Return the IEnumerator interface for KvpHash
///// </summary>
///// <returns>IEnumerator</returns>
//IEnumerator GetEnumerator();
/// <summary>
/// Gets the "Key" for the first ocurrence of "Value" in the KvpHash.
/// </summary>
/// <param name="Value"></param>
/// <returns>Key</returns>
dynamic GetKey(dynamic Value);
/// <summary>
/// Returns a variant array of the Keys of the KvpHash
/// </summary>
/// /// <returns>Variant Array</returns>
dynamic[] GetKeys();
/// <summary>
/// Returns a variant array of the values of the KvpHash
/// </summary>
/// <returns>Variant Array</returns>
dynamic[] GetValues();
/// <summary>
/// True if the "Key" exists in the keys of the KvpHash
/// </summary>
/// <param name="Key"></param>
/// <returns>Boolean</returns>
bool HoldsKey(dynamic Key);
/// <summary>
/// True if the "Value" exists in the values of the KvpHash
/// </summary>
/// <param name="Value"></param>
/// <returns>Boolean</returns>
bool HoldsValue(dynamic Value);
/// <summary>
/// True if the KvpHash holds 0 key/value pairs
/// </summary>
/// <returns>Boolean</returns>
bool IsEmpty();
/// <summary>
/// True if the KvpHash holds one or more key/value pairs
/// </summary>
/// <returns>Boolean</returns>
bool IsNotEmpty();
/// <summary>
/// True is the "Key" is not found in the keys of the KvpHash
/// </summary>
/// <param name="Key"></param>
/// <returns>Boolean</returns>
bool LacksKey(dynamic Key);
/// <summary>
/// True if the "Value" is not found in the values of the KvpHash
/// </summary>
/// <param name="Value"></param>
/// <returns>Boolean</returns>
bool LacksValue(dynamic Value);
/// <summary>
/// Reverses the Key/Value pairs in a KvpHash
/// </summary>
/// <returns>New KvpHash where:
/// KvpHash.Value(1) = KvpHash Unique values as Value/Key pairs
/// KvpHash.Value(2) = KvpHash Non unique values as Key/Value pairs</returns>
KvpHash Mirror();
/// <summary>
/// Removes the Key/Value pair spacified by "Key" from the KvpHash
/// </summary>
/// <param name="Key"></param>
void Remove(dynamic Key);
/// <summary>
/// Removes all Key/Value pairs from the KvpHash
/// </summary>
void RemoveAll();
/// <summary>
/// Returns true if the Values in KvpHash are unique.
/// </summary>
/// <returns>Boolean</returns>
bool ValuesAreUnique();
/// <summary>
/// Returns true if the Values in KvpHash are not unique.
/// </summary>
/// <returns>Boolean</returns>
bool ValuesAreNotUnique();
}
[Guid("87E5A539-FDB3-40D0-9CCD-C817F9893C08")]
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
public class KvpHash : IKvpHash, IEnumerable
{
private Hashtable MyKvpHash = new Hashtable();
public dynamic this[dynamic Key]
{
get
{
return MyKvpHash[Key];
}
set
{
MyKvpHash[Key] = value;
}
}
public void AddByIndex(dynamic Value)
{
int my_index = MyKvpHash.Count + 1;
while (MyKvpHash.ContainsKey(my_index))
{
my_index++;
}
MyKvpHash.Add(my_index, Value);
}
public void AddByIndexAsChars(string this_string)
{
int my_index = MyKvpHash.Count + 1;
while (MyKvpHash.ContainsKey(my_index))
{
my_index++;
}
char[] MyArray = this_string.ToCharArray();
MyKvpHash.Clear();
for (int i = 0; i <= MyArray.GetUpperBound(0); i++)
{
//KvpHash uses ordinal indexes
MyKvpHash.Add(i + 1, MyArray[i].ToString());
}
}
public void AddByIndexAsSubStr(string this_string, string this_seperator = ",")
{
int my_index = MyKvpHash.Count + 1;
while (MyKvpHash.ContainsKey(my_index))
{
my_index++;
}
string[] MyArray = this_string.Split(this_seperator.ToArray());
for (int i = 0; i <= MyArray.GetUpperBound(0); i++)
{
//KvpHash uses ordinal indexes
MyKvpHash.Add(i + 1, MyArray[i]);
}
}
public void AddByIndexFromArray(dynamic this_array)
{
int my_index = MyKvpHash.Count + 1;
while (MyKvpHash.ContainsKey(my_index))
{
my_index++;
}
for (int i = 0; i <= this_array.GetUpperBound(0); i++)
{
//KvpHash uses ordinal indexes
MyKvpHash.Add(i + 1, this_array[i]);
}
}
public void AddByKey(dynamic Key, dynamic Value)
{
MyKvpHash.Add(Key, Value);
}
public KvpHash Cohorts(KvpHash ArgKvpHash)
{
KvpHash ResultKvpHash = new KvpHash();
// VBA reports object not set error if the resuly KvpHash are not newed
for (int i = 1; i < 7; i++)
{
ResultKvpHash.AddByKey(i, new KvpHash());
}
foreach (DictionaryEntry MyKvpHashPair in MyKvpHash)
{
// A plus unique in B
ResultKvpHash[1].AddByKey(MyKvpHashPair.Key, MyKvpHashPair.Value);
if (ArgKvpHash.LacksKey(MyKvpHashPair.Key)) // problem is here
{
// In A only or in B only
ResultKvpHash[3].AddByKey(MyKvpHashPair.Key, MyKvpHashPair.Value);
// In A only
ResultKvpHash[5].AddByKey(MyKvpHashPair.Key, MyKvpHashPair.Value);
}
else
{
// In A and In B
ResultKvpHash[4].AddByKey(MyKvpHashPair.Key, MyKvpHashPair.Value);
}
}
foreach (dynamic MyKey in ArgKvpHash.GetKeys())
{
// B in A with different value
if (ResultKvpHash[1].LacksKey(MyKey)) // Result 0 will contain all of A
{
ResultKvpHash[1].AddByKey(MyKey, ArgKvpHash[MyKey]);
ResultKvpHash[3].AddByKey(MyKey, ArgKvpHash[MyKey]);
ResultKvpHash[6].AddByKey(MyKey, ArgKvpHash[MyKey]);
}
else
{
if (ResultKvpHash[1][MyKey] != ArgKvpHash[MyKey])
{
ResultKvpHash[2].AddByKey(MyKey, ArgKvpHash[MyKey]);
}
}
}
return ResultKvpHash;
}
public Int32 Count()
{
return MyKvpHash.Count;
}
public bool IsEmpty()
{
return MyKvpHash.Count == 0;
}
public bool IsNotEmpty()
{
return !IsEmpty();
}
public IEnumerator GetEnumerator()
{
foreach (DictionaryEntry my_pair in MyKvpHash)
{
yield return my_pair.Value;
}
}
public dynamic GetKey(dynamic Value)
{
return this.Mirror()[1][Value];
}
public dynamic[] GetKeys()
{
return (dynamic[]) MyKvpHash.Keys;
}
public dynamic[] GetValues()
{
return (dynamic[]) MyKvpHash.Values;
}
public bool HoldsKey(dynamic Key)
{
return MyKvpHash.ContainsKey(Key);
}
public bool HoldsValue(dynamic Value)
{
return MyKvpHash.ContainsValue(Value);
}
public bool LacksKey(dynamic Key)
{
return !HoldsKey(Key);
}
public bool LacksValue(dynamic Value)
{
return !HoldsValue(Value);
}
public KvpHash Mirror()
{
KvpHash MyResult = new KvpHash();
MyResult.AddByIndex(new KvpHash());
MyResult.AddByIndex(new KvpHash());
foreach (DictionaryEntry my_pair in MyKvpHash)
{
if (MyResult[1].LacksKey(my_pair.Value))
{
MyResult[1].AddByKey(my_pair.Value, my_pair.Key);
}
else
{
MyResult[2].AddByKey(my_pair.Key, my_pair.Value);
}
}
return MyResult;
}
public void Remove(dynamic Key)
{
MyKvpHash.Remove(Key);
}
public void RemoveAll()
{
MyKvpHash.Clear();
}
public bool ValuesAreUnique()
{
return MyKvpHash.Count == ((dynamic[]) MyKvpHash.Values).Distinct().Count();
}
public bool ValuesAreNotUnique()
{
return !ValuesAreUnique();
}
}
}
#Freeflow if You change in Word Module the myHt definition it will by OK and will work fine.
Public Sub TestKvpHash()
Dim myHt As Object
' KvpHash is a C# wrapper for a System.Collections.HashTable
Set myHt = New VBAExtensions.KvpHash
' Rest of code

SQL Server CLR function aggregation of sorted string

In order to get a sorted aggregated string, I wrote the CLR function below. However, it always returns empty instead of what I expected, just like "001, 002, 003". I tried to debug the CLR function in visual studio 2017, but threw the error message
The operation could not be completed. Unspecified error
Code:
[Serializable]
[SqlUserDefinedAggregate(
Format.UserDefined, //use clr serialization to serialize the intermediate result
Name = "CLRSortedCssvAgg", //aggregate name on sql
IsInvariantToNulls = true, //optimizer property
IsInvariantToDuplicates = false, //optimizer property
IsInvariantToOrder = false, //optimizer property
IsNullIfEmpty = false, //optimizer property
MaxByteSize = -1) //maximum size in bytes of persisted value
]
public class SortedCssvConcatenateAgg : IBinarySerialize
{
/// <summary>
/// The variable that holds all the strings to be aggregated.
/// </summary>
List<string> aggregationList;
StringBuilder accumulator;
/// <summary>
/// Separator between concatenated values.
/// </summary>
const string CommaSpaceSeparator = ", ";
/// <summary>
/// Initialize the internal data structures.
/// </summary>
public void Init()
{
accumulator = new StringBuilder();
aggregationList = new List<string>();
}
/// <summary>
/// Accumulate the next value, not if the value is null or empty.
/// </summary>
public void Accumulate(SqlString value)
{
if (value.IsNull || String.IsNullOrEmpty(value.Value))
{
return;
}
aggregationList.Add(value.Value);
}
/// <summary>
/// Merge the partially computed aggregate with this aggregate.
/// </summary>
/// <param name="other"></param>
public void Merge(SortedCssvConcatenateAgg other)
{
aggregationList.AddRange(other.aggregationList);
}
/// <summary>
/// Called at the end of aggregation, to return the results of the aggregation.
/// </summary>
/// <returns></returns>
public SqlString Terminate()
{
if (aggregationList != null && aggregationList.Count > 0)
{
aggregationList.Sort();
accumulator.Append(string.Join(CommaSpaceSeparator, aggregationList));
aggregationList.Clear();
}
return new SqlString(accumulator.ToString());
}
public void Read(BinaryReader r)
{
accumulator = new StringBuilder(r.ReadString());
}
public void Write(BinaryWriter w)
{
w.Write(accumulator.ToString());
}
}
You are close. Just need a few minor adjustments. Do the following and it will work (I tested it):
Remove all references to accumulator. It is not used.
Replace the Terminate(), Read(), and Write() methods with the following:
public SqlString Terminate()
{
string _Aggregation = null;
if (aggregationList != null && aggregationList.Count > 0)
{
aggregationList.Sort();
_Aggregation = string.Join(CommaSpaceSeparator, aggregationList);
}
return new SqlString(_Aggregation);
}
public void Read(BinaryReader r)
{
int _Count = r.ReadInt32();
aggregationList = new List<string>(_Count);
for (int _Index = 0; _Index < _Count; _Index++)
{
aggregationList.Add(r.ReadString());
}
}
public void Write(BinaryWriter w)
{
w.Write(aggregationList.Count);
foreach (string _Item in aggregationList)
{
w.Write(_Item);
}
}
That said, I'm not sure if this approach is faster or slower than the FOR XML approach, but a UDA certainly makes for a more readable query, especially if you need multiple aggregations.
Still, I should mention that starting in SQL Server 2017, this became a built-in function: STRING_AGG (which allows for sorting via the WITHIN GROUP (ORDER BY ... ) clause).
In your Accumulate and Merge, you're dealing with your aggregationList; in Read and Write you're dealing with accumulator. You should pick one or the other for all of them and use it. As I understand it, Read and Write are used when the engine needs to persist temporary results to a work table. For your case, when it does that, it's persisting only your empty StringBuilder.

How to convert hierarchical key value pairs from a string into json with c#?

I have the following http post body sent to a asp.net web api via a web hook from chargify.
id=38347752&event=customer_update&payload[customer][address]=qreweqwrerwq&payload[customer][address_2]=qwerewrqew&payload[customer][city]=ererwqqerw&payload[customer][country]=GB&payload[customer][created_at]=2015-05-14%2004%3A46%3A48%20-0400&payload[customer][email]=a%40test.com&payload[customer][first_name]=Al&payload[customer][id]=8619620&payload[customer][last_name]=Test&payload[customer][organization]=&payload[customer][phone]=01&payload[customer][portal_customer_created_at]=2015-05-14%2004%3A46%3A49%20-0400&payload[customer][portal_invite_last_accepted_at]=&payload[customer][portal_invite_last_sent_at]=2015-05-14%2004%3A46%3A49%20-0400&payload[customer][reference]=&payload[customer][state]=&payload[customer][updated_at]=2015-05-14%2011%3A25%3A19%20-0400&payload[customer][verified]=false&payload[customer][zip]=&payload[site][id]=26911&payload[site][subdomain]=testsubdomain
How do i convert this payload[customer][address]=value etc. to a json string using c#?
You current problem
How to convert chargify webhooks to json with c#?
can be generalized to
How to extract key value pairs from a string, convert them into the corresponding hierarchy and return them in JSON?
To answer your question:
string rawData = "id=38347752&event=customer_update&payload[customer][address]=qreweqwrerwq&payload[customer][address_2]=qwerewrqew&payload[customer][city]=ererwqqerw&payload[customer][country]=GB&payload[customer][created_at]=2015-05-14%2004%3A46%3A48%20-0400&payload[customer][email]=a%40test.com&payload[customer][first_name]=Al&payload[customer][id]=8619620&payload[customer][last_name]=Test&payload[customer][organization]=&payload[customer][phone]=01&payload[customer][portal_customer_created_at]=2015-05-14%2004%3A46%3A49%20-0400&payload[customer][portal_invite_last_accepted_at]=&payload[customer][portal_invite_last_sent_at]=2015-05-14%2004%3A46%3A49%20-0400&payload[customer][reference]=&payload[customer][state]=&payload[customer][updated_at]=2015-05-14%2011%3A25%3A19%20-0400&payload[customer][verified]=false&payload[customer][zip]=&payload[site][id]=26911&payload[site][subdomain]=testsubdomain";
ChargifyWebHook webHook = new ChargifyWebHook(rawData);
JSONNode node = new JSONNode("RootOrWhatEver");
foreach (KeyValuePair<string, string> keyValuePair in webHook.KeyValuePairs)
{
node.InsertInHierarchy(ChargifyWebHook.ExtractHierarchyFromKey(keyValuePair.Key), keyValuePair.Value);
}
string result = node.ToJSONObject();
With your specified input the result looks like this (without line breaks):
{
"id": "38347752",
"event": "customer_update",
"payload": {
"customer": {
"address": "qreweqwrerwq",
"address_2": "qwerewrqew",
"city": "ererwqqerw",
"country": "GB",
"created_at": "2015-05-14 04:46:48 -0400",
"email": "a#test.com",
"first_name": "Al",
"id": "8619620",
"last_name": "Test",
"organization": "",
"phone": "01",
"portal_customer_created_at": "2015-05-14 04:46:49 -0400",
"portal_invite_last_accepted_at": "",
"portal_invite_last_sent_at": "2015-05-14 04:46:49 -0400",
"reference": "",
"state": "",
"updated_at": "2015-05-14 11:25:19 -0400",
"verified": "false",
"zip": ""
},
"site": {
"id": "26911",
"subdomain": "testsubdomain"
}
}
}
As your problem is not limited to 1, 2 or 3 levels you clearly need a recursive solution. Therefore I created a JSONNode class which is able to insert children by specifying the hierarchy as a List<string>.
If you take A.B.C as an example, at the beginning the method InsertIntoHierarchy checks whether more levels are needed or not (depending on the length of the entries specified, in our case we would get a list containing A, B and C), if so it inserts a child (used as container) with the specified name of the level and passes the problem on to this child. Of course the name of the current recursion level is removed during that step so according to our example the container with the name A would have been added and the list containing B and C would have been passed on to this container. If the last level of recursion is reached, a node containing the name and the value will be inserted.
To get the solution working you will need the following 2 classes:
ChargifyWebHook
/// <summary>
/// Represents the chargify web hook class.
/// </summary>
public class ChargifyWebHook
{
/// <summary>
/// Indicates whether the raw data has already been parsed or not.
/// </summary>
private bool initialized;
/// <summary>
/// Contains the key value pairs extracted from the raw data.
/// </summary>
private Dictionary<string, string> keyValuePairs;
/// <summary>
/// Initializes a new instance of the <see cref="ChargifyWebHook"/> class.
/// </summary>
/// <param name="data">The raw data of the web hook.</param>
/// <exception cref="System.ArgumentException">Is thrown if the sepcified raw data is null or empty.</exception>
public ChargifyWebHook(string data)
{
if (String.IsNullOrEmpty(data))
{
throw new ArgumentException("The specified value must neither be null nor empty", data);
}
this.initialized = false;
this.keyValuePairs = new Dictionary<string, string>();
this.RawData = data;
}
/// <summary>
/// Gets the raw data of the web hook.
/// </summary>
public string RawData
{
get;
private set;
}
/// <summary>
/// Gets the key value pairs contained in the raw data.
/// </summary>
public Dictionary<string, string> KeyValuePairs
{
get
{
if (!initialized)
{
this.keyValuePairs = ExtractKeyValuesFromRawData(this.RawData);
initialized = true;
}
return this.keyValuePairs;
}
}
/// <summary>
/// Extracts the key value pairs from the specified raw data.
/// </summary>
/// <param name="rawData">The data which contains the key value pairs.</param>
/// <param name="keyValuePairSeperator">The pair seperator, default is '&'.</param>
/// <param name="keyValueSeperator">The key value seperator, default is '='.</param>
/// <returns>The extracted key value pairs.</returns>
/// <exception cref="System.FormatException">Is thrown if an key value seperator is missing.</exception>
public static Dictionary<string, string> ExtractKeyValuesFromRawData(string rawData, char keyValuePairSeperator = '&', char keyValueSeperator = '=')
{
Dictionary<string, string> keyValuePairs = new Dictionary<string, string>();
string[] rawDataParts = rawData.Split(new char[] { keyValuePairSeperator });
foreach (string rawDataPart in rawDataParts)
{
string[] keyAndValue = rawDataPart.Split(new char[] { keyValueSeperator });
if (keyAndValue.Length != 2)
{
throw new FormatException("The format of the specified raw data is incorrect. Key value pairs in the following format expected: key=value or key1=value1&key2=value2...");
}
keyValuePairs.Add(Uri.UnescapeDataString(keyAndValue[0]), Uri.UnescapeDataString(keyAndValue[1]));
}
return keyValuePairs;
}
/// <summary>
/// Extracts the hierarchy from the key, e.g. A[B][C] will result in A, B and C.
/// </summary>
/// <param name="key">The key who's hierarchy shall be extracted.</param>
/// <param name="hierarchyOpenSequence">Specifies the open sequence for the hierarchy speration.</param>
/// <param name="hierarchyCloseSequence">Specifies the close sequence for the hierarchy speration.</param>
/// <returns>A list of entries for the hierarchy names.</returns>
public static List<string> ExtractHierarchyFromKey(string key, string hierarchyOpenSequence = "[", string hierarchyCloseSequence = "]")
{
if (key.Contains(hierarchyOpenSequence) && key.Contains(hierarchyCloseSequence))
{
return key.Replace(hierarchyCloseSequence, string.Empty).Split(new string[] { hierarchyOpenSequence }, StringSplitOptions.None).ToList();
}
if (key.Contains(hierarchyOpenSequence) && !key.Contains(hierarchyCloseSequence))
{
return key.Split(new string[] { hierarchyOpenSequence }, StringSplitOptions.None).ToList();
}
if (!key.Contains(hierarchyOpenSequence) && key.Contains(hierarchyCloseSequence))
{
return key.Split(new string[] { hierarchyCloseSequence }, StringSplitOptions.None).ToList();
}
return new List<string>() { key };
}
}
JSONNode
/// <summary>
/// Represents the JSONNode class.
/// </summary>
public class JSONNode
{
/// <summary>
/// Initializes a new instance of the <see cref="JSONNode"/> class.
/// </summary>
/// <param name="name">The name of the node.</param>
/// <param name="value">The value of the node.</param>
public JSONNode(string name, string value)
{
this.Name = name;
this.Value = value;
this.Children = new Dictionary<string, JSONNode>();
}
/// <summary>
/// Initializes a new instance of the <see cref="JSONNode"/> class.
/// </summary>
/// <param name="name">The name of the node.</param>
public JSONNode(string name)
: this(name, string.Empty)
{
}
/// <summary>
/// Gets the name of the node.
/// </summary>
public string Name
{
get;
private set;
}
/// <summary>
/// Gets the children of the node.
/// </summary>
public Dictionary<string, JSONNode> Children
{
get;
private set;
}
/// <summary>
/// Gets the value of the node.
/// </summary>
public string Value
{
get;
private set;
}
/// <summary>
/// Inserts a new node in the corresponding hierarchy.
/// </summary>
/// <param name="keyHierarchy">A list with entries who specify the hierarchy.</param>
/// <param name="value">The value of the node.</param>
/// <exception cref="System.ArgumentNullException">Is thrown if the keyHierarchy is null.</exception>
/// <exception cref="System.ArgumentException">Is thrown if the keyHierarchy is empty.</exception>
public void InsertInHierarchy(List<string> keyHierarchy, string value)
{
if (keyHierarchy == null)
{
throw new ArgumentNullException("keyHierarchy");
}
if (keyHierarchy.Count == 0)
{
throw new ArgumentException("The specified hierarchy list is empty", "keyHierarchy");
}
// If we are not in the correct hierarchy (at the last level), pass the problem
// to the child.
if (keyHierarchy.Count > 1)
{
// Extract the current hierarchy level as key
string key = keyHierarchy[0];
// If the key does not already exists - add it as a child.
if (!this.Children.ContainsKey(key))
{
this.Children.Add(key, new JSONNode(key));
}
// Remove the current hierarchy from the list and ...
keyHierarchy.RemoveAt(0);
// ... pass it on to the just inserted child.
this.Children[key].InsertInHierarchy(keyHierarchy, value);
return;
}
// If we are on the last level, just insert the node with it's value.
this.Children.Add(keyHierarchy[0], new JSONNode(keyHierarchy[0], value));
}
/// <summary>
/// Gets the textual representation of this node as JSON entry.
/// </summary>
/// <returns>A textual representaiton of this node as JSON entry.</returns>
public string ToJSONEntry()
{
// If there is no child, return the name and the value in JSON format.
if (this.Children.Count == 0)
{
return string.Format("\"{0}\":\"{1}\"", this.Name, this.Value);
}
// Otherwise there are childs so return all of them formatted as object.
StringBuilder builder = new StringBuilder();
builder.AppendFormat("\"{0}\":", this.Name);
builder.Append(this.ToJSONObject());
return builder.ToString();
}
/// <summary>
/// Gets the textual representation of this node as JSON object.
/// </summary>
/// <returns>A textual representaiton of this node as JSON object.</returns>
public string ToJSONObject()
{
StringBuilder builder = new StringBuilder();
builder.Append("{");
foreach (JSONNode value in this.Children.Values)
{
builder.Append(value.ToJSONEntry());
builder.Append(",");
}
builder.Remove(builder.Length - 1, 1);
builder.Append("}");
return builder.ToString();
}
}

Huffman Tree: Traversing [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 8 years ago.
Improve this question
I'm not sure how I'm going to attack the traversing of my Huffman Tree. The tree is correct, I just have a hard time figuring out how to traverse it in a good way. For some reason, my traversing method gives no result...
UPDATE: Cleaned up the code, made it more Object Oriented
Node class:
public class Node
{
public int frekvens; //Frequency
public char tegn; //Symbol
public Node venstre; //Left child
public Node høyre; //Right child
public string s; //result string
public string resultat;
public Node (char c) // Node constructor containing symbol.
{
frekvens = 1;
tegn = c;
}
public Node (int f, Node venstre, Node høyre) // Node Constructor containing frequency and children
{
frekvens = f;
this.venstre = venstre;
this.høyre = høyre;
}
public Node (Node node) // Node constructor containing a node
{
frekvens = node.frekvens;
tegn = node.tegn;
this.venstre = venstre;
this.høyre = høyre;
}
public void ØkMed1() // Inkrement frequency by one
{
frekvens = frekvens + 1;
}
public char getVenstreTegn ()
{
return venstre.tegn;
}
public char getHøyreTegn ()
{
return venstre.tegn;
}
public int getVenstreFrekvens ()
{
return venstre.frekvens;
}
public int getHøyreFrekvens ()
{
return høyre.frekvens;
}
public int getFrekvens()
{
return frekvens;
}
public bool ErTegn(char c)
{
if ( c == tegn)
{
return false;
}
else
{
return true;
}
}
//Pretty sure this does not work as intended
public string traverser (Node n) //Traverse the tree
{
if (n.tegn != '\0') //If the node containes a symbol --> a leaf
{
resultat += s;
}
else
{
if (n.getVenstreTegn() == '\0') //If left child does not have a symbol
{
s += "0";
traverser(n.venstre);
}
if (n.getHøyreTegn() == '\0') //If right child does not have a symbol
{
s += "1";
traverser(n.høyre);
}
}
return resultat;
}
public string Resultat() //Used priviously to check if i got the correct huffman tree
{
string resultat;
resultat = "Tegn: " + Convert.ToString(tegn) +" frekvens: " + Convert.ToString(frekvens) + "\n";
return resultat;
}
}
Huffman_Tree Class:
public class Huffman_Tre
{
string treString;
List<Node> noder = new List<Node>();
public Node rot;
public void bygg (string input)
{
bool funnet; //Found
char karakter; //character
for (int i = 0; i < input.Length;i++) //Loops through string and sets character
//with coresponding freqeuncy in the node list
{
karakter = input[i];
funnet = false; //default
for (int j = 0; j< noder.Count; j++)
{
if (noder[j].ErTegn(karakter) == false) //if the character already exists
{
noder[j].ØkMed1(); //inkrement frequency by one
funnet = true;
break;
}
}
if (!funnet) //if the character does not exist
{
noder.Add(new Node(karakter)); //add the character to list
}
}
//Sorting node list acending by frequency
var sortertListe = noder.OrderBy(c => c.frekvens).ToList();
noder = sortertListe;
do
{
noder.Add(new Node((noder[0].frekvens + noder[1].frekvens), noder[0],noder[1]));
//Remove the leaf nodes
noder.RemoveAt(0);
noder.RemoveAt(0);
} while(noder.Count >= 2);
}
public Node getRot()
{
return rot;
}
public string visTre()
{
foreach (Node node in noder)
{
treString += node.Resultat();
}
return treString;
}
public bool erNull()
{
if (noder[0].tegn == '\0')
{
return true;
}
else
return false;
}
}
Main Program:
private void btnKomprimer_Click(object sender, System.Windows.RoutedEventArgs e)
{
string input; //The string input I want to compress
input = txtInput.Text; //initialize input to text input
input = input.ToLower();
txtOutput.Text = "";
Huffman_Tre tre = new Huffman_Tre();
tre.bygg(input);
Node rot = new Node(tre.getRot());
txtOutput.Text += rot.traverser(rot);
}
}
As I had a little bit of time left, I worked out an example of a Huffman tree, while playing with C# 6.0. It's not optimized (not even by far!), but it works fine as an example. And it will help you to look where your 'challenge' may arise. As my English is far better than my Scandinavian knowledge, I used English naming, I hope you don't mind.
First, let's start with the class that keeps the frequencies.
public sealed class HuffmanFrequencyTable
{
#region Properties
/// <summary>
/// Holds the characters and their corresponding frequencies
/// </summary>
public Dictionary<char, int> FrequencyTable { get; set; } = new Dictionary<char, int>();
#endregion
#region Methods
/// <summary>
/// Clears the internal frequency table
/// </summary>
public void Clear()
{
FrequencyTable?.Clear();
}
/// <summary>
/// Accepts and parses a new line (string) which is then
/// merged with the existing dictionary or frequency table
/// </summary>
/// <param name="line">The line to parse</param>
public void Accept(string line)
{
if (!string.IsNullOrEmpty(line))
{
line.GroupBy(ch => ch).
ToDictionary(g => g.Key, g => g.Count()).
ToList().
ForEach(x => FrequencyTable[x.Key] = x.Value);
}
}
/// <summary>
/// Performs a dump of the frequency table, ordering all characters, lowest frequency first.
/// </summary>
/// <returns>The frequency table in the format 'character [frequency]'</returns>
public override string ToString()
{
return FrequencyTable?.PrintFrequencies();
}
#endregion
}
Please note that the ToString() method uses an extension method that is able to 'dump' the contents of the Dictionary used. The extensions is located in a static class called Helpers and looks like this:
/// <summary>
/// Extension method that helps to write the contents of a generic Dictionary to a string, ordered by it's values and
/// printing the key and it's value between brackets.
/// </summary>
/// <typeparam name="TKey">Generic key</typeparam>
/// <typeparam name="TValue">Generic value type</typeparam>
/// <param name="dictionary">The dictionary</param>
/// <exception cref="ArgumentNullException">Throws an argument null exception if the provided dictionary is null</exception>
/// <returns></returns>
public static string PrintFrequencies<TKey, TValue>(this IDictionary<TKey, TValue> dictionary)
{
if (dictionary == null)
throw new ArgumentNullException("dictionary");
var items = from kvp in dictionary
orderby kvp.Value
select kvp.Key + " [" + kvp.Value + "]";
return string.Join(Environment.NewLine, items);
}
Now, with this FrequencyTable in place, we can start looking on how to build up the Nodes. Huffman works with a binary tree, so it's best to generate a Node class having a left and right child node. I also took the liberty to perform the traversal algorithm here as well. This class is built up as following:
public sealed class HuffmanNode
{
#region Properties
/// <summary>
/// Holds the left node, if applicable, otherwise null
/// </summary>
public HuffmanNode Left { get; set; } = null;
/// <summary>
/// Holds the right node, if applicable, otherwise null
/// </summary>
public HuffmanNode Right { get; set; } = null;
/// <summary>
/// Holds the Character (or null) for this particular node
/// </summary>
public char? Character { get; set; } = null;
/// <summary>
/// Holds the frequency for this particular node, defaulted to 0
/// </summary>
public int Frequency { get; set; } = default(int);
#endregion
#region Methods
/// <summary>
/// Traverses all nodes recursively returning the binary
/// path for the corresponding character that has been found.
/// </summary>
/// <param name="character">The character to find</param>
/// <param name="data">The datapath (containing '1's and '0's)</param>
/// <returns>The complete binary path for a character within a node</returns>
public List<bool> Traverse(char? character, List<bool> data)
{
//Check the leafs for existing characters
if (null == Left && null == Right)
{
//We're at an endpoint of our 'tree', so return it's data or nothing when the symbol
//characters do not match
return (bool)character?.Equals(Character) ? data : null;
}
else
{
List<bool> left = null;
List<bool> right = null;
//TODO: If possible refactor with proper C# 6.0 features
if (null != Left)
{
List<bool> leftPath = new List<bool>(data);
leftPath.Add(false); //Add a '0'
left = Left.Traverse(character, leftPath); //Recursive traversal for child nodes within this left node.
}
if (null != Right)
{
List<bool> rightPath = new List<bool>(data);
rightPath.Add(true); //Add a '1'
right = Right.Traverse(character, rightPath); //Recursive traversal for childnodes within this right node
}
return (null != left) ? left : right;
}
}
#endregion
}
I use the Node class within the HuffmanTree class. As, logically, a tree is built up from nodes. The corresponding HuffmanTree is written this way:
public sealed class HuffmanTree
{
#region Fields
/// <summary>
/// Field for keeping the Huffman nodes in. Internally used.
/// </summary>
private List<HuffmanNode> nodes = new List<HuffmanNode>();
#endregion
#region Properties
/// <summary>
/// Holds the Huffman tree
/// </summary>
public HuffmanNode Root { get; set; } = null;
/// <summary>
/// Holds the frequency table for all parsed characters
/// </summary>
public HuffmanFrequencyTable Frequencies { get; private set; } = new HuffmanFrequencyTable()
/// <summary>
/// Holds the amount of bits after encoding the tree.
/// Primary usable for decoding.
/// </summary>
public int BitCountForTree { get; private set; } = default(int);
#endregion
#region Methods
/// <summary>
/// Builds the Huffman tree
/// </summary>
/// <param name="source">The source to build the Hufftree from</param>
/// <exception cref="ArgumentNullException">Thrown when source is null or empty</exception>
public void BuildTree(string source)
{
nodes.Clear(); //As we build a new tree, first make sure it's clean :)
if (string.IsNullOrEmpty(source))
throw new ArgumentNullException("source");
else
{
Frequencies.Accept(source);
foreach (KeyValuePair<char, int> symbol in Frequencies.FrequencyTable)
{
nodes.Add(new HuffmanNode() { Character = symbol.Key, Frequency = symbol.Value });
}
while (nodes.Count > 1)
{
List<HuffmanNode> orderedNodes = nodes.OrderBy(node => node.Frequency).ToList();
if (orderedNodes.Count >= 2)
{
List<HuffmanNode> takenNodes = orderedNodes.Take(2).ToList();
HuffmanNode parent = new HuffmanNode()
{
Character = null,
Frequency = takenNodes[0].Frequency + takenNodes[1].Frequency,
Left = takenNodes[0],
Right = takenNodes[1]
};
//Remove the childnodes from the original node list and add the new parent node
nodes.Remove(takenNodes[0]);
nodes.Remove(takenNodes[1]);
nodes.Add(parent);
}
}
Root = nodes.FirstOrDefault();
}
}
/// <summary>
/// Encodes a given string to the corresponding huffman encoding path
/// </summary>
/// <param name="source">The source to encode</param>
/// <returns>The binary huffman representation of the source</returns>
public BitArray Encode(string source)
{
if (!string.IsNullOrEmpty(source))
{
List<bool> encodedSource = new List<bool>();
//Traverse the tree for each character in the passed source (string) and add the binary path to the encoded source
encodedSource.AddRange(source.SelectMany(character =>
Root.Traverse(character, new List<bool>())
).ToList()
);
//For decoding, we might need the amount of bits to skip trailing bits.
BitCountForTree = encodedSource.Count;
return new BitArray(encodedSource.ToArray());
}
else return null;
}
/// <summary>
/// Decodes a given binary path to represent it's string value
/// </summary>
/// <param name="bits">BitArray for traversing the tree</param>
/// <returns></returns>
public string Decode(BitArray bits)
{
HuffmanNode current = Root;
string decodedString = string.Empty;
foreach (bool bit in bits)
{
//Find the correct current node depending on the bit set or not set.
current = (bit ? current.Right ?? current : current.Left ?? current);
if (current.IsLeaf())
{
decodedString += current.Character;
current = Root;
}
}
return decodedString;
}
#endregion
}
What is interesting in this code, is that I decided to use BitArrays that will hold the binary paths for the tree when it's build up. The public BitArray Encode(string source) method here contains a dirty hack. I keep track of the total amount of bits used for encoding and store this within the BitCountForTree property. When performing a decode, I'll use this property to remove any trailing bits that may arise. There is a way nicer way to perform this, but I'll leave that open for you to find out.
Also, this class makes use of an extension method written for the HuffmanNode. It's a simple one though:
/// <summary>
/// Determines whether a given Huffman node is a leaf or not.
/// A node is considered to be a leaf when it has no childnodes
/// </summary>
/// <param name="node">A huffman node</param>
/// <returns>True if no children are left, false otherwise</returns>
public static bool IsLeaf(this HuffmanNode node)
{
return (null == node.Left && null == node.Right);
}
This extension method is convenient to determine whether or not a given node is actually a leafnode. A leaf is a node which has no childnodes left and thus the end of a binary tree (or better a branch of that tree).
Now the interesting part, how do I make things work here. I have build a Windows Forms application having 3 textboxes. One for the actual input, one for the binary (encoded) output and the last for showing the compressed result.
I also placed two simple buttons, one to perform the Huffman encoding and one for the Huffman decoding.
The Huffman encoding method is written as following (just in the eventhandler of the encode button):
string input = tbInput.Text;
Tree.BuildTree(input); //Build the huffman tree
BitArray encoded = Tree.Encode(input); //Encode the tree
//First show the generated binary output
tbBinaryOutput.Text = string.Join(string.Empty, encoded.Cast<bool>().Select(bit => bit ? "1" : "0"));
//Next, convert the binary output to the new characterized output string.
byte[] bytes = new byte[(encoded.Length / 8) + 1];
encoded.CopyTo(bytes, 0);
tbOutput.Text = Encoding.Default.GetString(bytes); //Write the compressed output to the textbox.
Note that the encoded binary string does not have any trailing bits. I'll leave that up to the Encoding mechanisms of C#. The downside of this, is that I have to keep track of it when decoding.
The decoding is not too hard now as well. Although, for this example, I am making use of the compressed output generated by the encoding code placed above. Also, I am assuming that the Huffman tree (and it's frequency table!!!) are already built. Normally, the frequency table is stored within the compressed file, so that it can be rebuild.
//First convert the compressed output to a bit array again again and skip trailing bits.
bool[] boolAr = new BitArray(Encoding.Default.GetBytes(tbOutput.Text)).Cast<bool>().Take(Tree.BitCountForTree).ToArray();
BitArray encoded = new BitArray( boolAr );
string decoded = Tree.Decode(encoded);
MessageBox.Show(decoded, "Decoded result: ", MessageBoxButtons.OK, MessageBoxIcon.Information);
Please pay attention to the dirty hack I created, as the Encoding.Default.GetBytes(tbOutput.Text) surely generates a byte array, it may contain trailing bits which need not to be decoded. Hence that I only take the amount of bits that I will actually need, based upon the rebuild tree.
So when running, my example provides the following output, when using the 'world renown sentence' "The quick brown fox jumps over the lazy programmer":
After pressing the "Huff encode" button:
And after pressing the "Huff decode" button:
Now this code can really use some optimizations, as you might consider using Arrays instead of Dictionaries. There are more, but it's up to you for consideration.

Categories

Resources