Skip to content

Commit

Permalink
Resolved #34: (ShippingByTotal) added the ability to define zip range…
Browse files Browse the repository at this point in the history
…s including wildcards.
  • Loading branch information
muratcakir committed Sep 13, 2013
1 parent 805bc54 commit 77cadc2
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 38 deletions.
197 changes: 175 additions & 22 deletions src/Libraries/SmartStore.Core/Utilities/Wildcard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,205 @@
namespace SmartStore.Utilities
{

public class Wildcard
/// <summary>
/// This class is used to use wildcards and number ranges while
/// searching in text. * is used of any chars, ? for one char and
/// a number range is used with the - char. (12-232)
/// </summary>
public class Wildcard : Regex
{
private readonly Regex _regex;
//private readonly string _pattern;
#region Fields
/// <summary>
/// This flag determines whether the parser is forward
/// direction or not.
/// </summary>
private static bool m_isForward;
#endregion

public Wildcard(string pattern)
: this(pattern, RegexOptions.None)
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="Wildcard"/> class.
/// </summary>
/// <param name="pattern">The wildcard pattern.</param>
public Wildcard(string pattern) : base(WildcardToRegex(pattern))
{
}

public Wildcard(string pattern, RegexOptions options)
/// <summary>
/// Initializes a new instance of the <see cref="Wildcard"/> class.
/// </summary>
/// <param name="pattern">The wildcard pattern.</param>
/// <param name="options">The regular expression options.</param>
public Wildcard(string pattern, RegexOptions options) : base(WildcardToRegex(pattern), options)
{
Guard.ArgumentNotEmpty(() => pattern);
_regex = new Regex(WildcardToRegex(pattern), options);
}
#endregion

public bool IsMatch(string input)
#region Private Implementation
/// <summary>
/// Searches all number range terms and converts them
/// to a regular expression term.
/// </summary>
/// <param name="pattern">The wildcard pattern.</param>
/// <returns>A converted regular expression term.</returns>
private static string WildcardToRegex(string pattern)
{
Guard.ArgumentNotEmpty(() => input);
return _regex.IsMatch(input);
m_isForward = true;
//escape and beginning
pattern = "^" + Regex.Escape(pattern);
//replace * with .*
pattern = pattern.Replace("\\*", ".*");
//$ is for end position and replace ? with a .
pattern = pattern.Replace("\\?", ".") + "$";

//convert the number ranges into regular expression
Regex re = new Regex("[0-9]+-[0-9]+");
MatchCollection collection = re.Matches(pattern);
foreach (Match match in collection)
{
string[] split = match.Value.Split(new char[] { '-' });
int min = Int32.Parse(split[0]);
int max = Int32.Parse(split[1]);

pattern = pattern.Replace(match.Value, ConvertNumberRange(min, max));
}

return pattern;
}

public static bool IsMatch(string input, string pattern)
/// <summary>
/// Converts the number range into regular expression term.
/// </summary>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <returns>The regular expression pattern for the number range term.</returns>
private static string ConvertNumberRange(int min, int max)
{
return IsMatch(input, pattern, RegexOptions.None);
if (max < min)
{
throw new InvalidOperationException("The minimum value could not be greater than the maximum value.");
}

string pattern = string.Empty;
int currentValue = min;
int tempMax = 0;
int radix = 1;

while (m_isForward ||
radix != 0)
{
tempMax = GetNextMaximum(currentValue, max, radix);
if (tempMax >= currentValue)
{
pattern += ParseRange(currentValue, tempMax, radix);
if (!(!m_isForward && radix == 1))
{
pattern += "|";
}
}
radix += (m_isForward ? 1 : -1);
currentValue = tempMax + 1;
}

//add a negative look behind and a negative look ahead in order
//to avoid that 122-321 is found in 2308.
return @"((?<!\d)(" + pattern + @")(?!\d))";
}

public static bool IsMatch(string input, string pattern, RegexOptions options)
/// <summary>
/// Gets the next maximum value in condition to the current value.
/// </summary>
/// <param name="currentValue">The current value.</param>
/// <param name="maximum">The absolute maximum.</param>
/// <param name="radix">The radix.</param>
/// <returns>The next number which is greater than the current value,
/// but less or equal than the absolute maximum value.
/// </returns>
private static int GetNextMaximum(int currentValue, int maximum, int radix)
{
Guard.ArgumentNotNull(() => input);
return new Wildcard(pattern, options).IsMatch(input);
//backward
if (!m_isForward)
{
if (radix != 1)
{
return ((maximum / (int)Math.Pow(10, radix - 1))) * (int)Math.Pow(10, radix - 1) - 1;
}
//end is reached
return maximum;
}
//forward
int tempMax = ((currentValue / (int)Math.Pow(10, radix)) + 1) * (int)Math.Pow(10, radix) - 1;
if (tempMax > maximum)
{
m_isForward = false;
radix--;
tempMax = ((maximum / (int)Math.Pow(10, radix))) * (int)Math.Pow(10, radix) - 1;
}

return tempMax;
}

private static string WildcardToRegex(string pattern)
/// <summary>
/// Parses the range and converts it into a regular expression term.
/// </summary>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <param name="radix">The radix form where up the value is changed.</param>
/// <returns>The pattern which descripes the specified range.</returns>
private static string ParseRange(int min, int max, int radix)
{
return "^" + Regex.Escape(pattern)
.Replace("\\*", ".*")
.Replace("\\?", ".") + "$";
string pattern = string.Empty;
int length = max.ToString().Length;

pattern += min.ToString().Substring(0, length - radix);
int minDigit = ExtractDigit(min, length - radix);
int maxDigit = ExtractDigit(max, length - radix);

pattern += GetRangePattern(minDigit, maxDigit, 1);
pattern += GetRangePattern(0, 9, radix - 1);

return pattern;
}

public override string ToString()
/// <summary>
/// Gets the range pattern for the specified figures.
/// </summary>
/// <param name="beginDigit">The begin digit.</param>
/// <param name="endDigit">The end digit.</param>
/// <param name="count">The count of the pattern.</param>
/// <returns>The pattern for the atomic number range.</returns>
private static string GetRangePattern(int beginDigit, int endDigit, int count)
{
return _regex.ToString();
if (count == 0)
{
return string.Empty;
}

if (beginDigit == endDigit)
{
return beginDigit.ToString();
}

string pattern = string.Format("[{0}-{1}]", beginDigit, endDigit);
if (count > 1)
{
pattern += string.Format("{{{0}}}", count);
}

return pattern;
}

/// <summary>
/// Extracts the digit form the value.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="digit">The digit.</param>
/// <returns>The figure at the specified digit.</returns>
private static int ExtractDigit(int value, int digit)
{
return Int32.Parse(value.ToString()[digit].ToString());
}
#endregion
}

}
4 changes: 2 additions & 2 deletions src/Plugins/Shipping.ByTotal/Localization/resources.de-de.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
<Value>Bundesland/Region/Kanton des Kunden. Wählen Sie Stern (*), wenn die Gebühr unabhängig von Region für alle Kunden im definierten Land gelten soll.</Value>
</LocaleResource>
<LocaleResource Name="Fields.Zip">
<Value>PLZ</Value>
<Value>PLZ-(Bereich)</Value>
</LocaleResource>
<LocaleResource Name="Fields.Zip.Hint">
<Value>PLZ des Kunden. Lassen Sie das Feld leer, wenn die Gebühr unabhängig von PLZ für alle Kunden im definierten (Bundes)land gelten soll.</Value>
<Value>PLZ-(Bereich) des Kunden, entweder als spezifischer Wert oder als Muster (z.B. 4000-49999 für das PLZ-Gebiet 4). In einem Muster lassen sich auch Wildcards verwenden, wie Stern (*) oder Fragezeichen (?). Lassen Sie das Feld leer, wenn die Gebühr unabhängig von PLZ für alle Kunden im definierten (Bundes)land gelten soll.</Value>
</LocaleResource>
<LocaleResource Name="Fields.ShippingMethod">
<Value>Versandart</Value>
Expand Down
4 changes: 2 additions & 2 deletions src/Plugins/Shipping.ByTotal/Localization/resources.en-us.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
<Value>If an asterisk is selected, then this shipping rate will apply to all customers from the given country, regardless of the state / province.</Value>
</LocaleResource>
<LocaleResource Name="Fields.Zip">
<Value>Zip</Value>
<Value>Zip-(Range)</Value>
</LocaleResource>
<LocaleResource Name="Fields.Zip.Hint">
<Value>Zip / postal code. If zip is empty, then this shipping rate will apply to all customers from the given country or state / province, regardless of the zip code.</Value>
<Value>Zip/postal code (range), either as specific value or range pattern (e.g. 4000-4999). You can also define wildcard chars like * or ?. If zip is empty, then this shipping rate will apply to all customers from the given country or state / province, regardless of the zip code.</Value>
</LocaleResource>
<LocaleResource Name="Fields.ShippingMethod">
<Value>Shipping method</Value>
Expand Down
44 changes: 32 additions & 12 deletions src/Plugins/Shipping.ByTotal/Services/ShippingByTotalService.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using SmartStore.Core.Caching;
using SmartStore.Core.Data;
using SmartStore.Plugin.Shipping.ByTotal.Domain;
using SmartStore.Utilities;

namespace SmartStore.Plugin.Shipping.ByTotal.Services
{
Expand Down Expand Up @@ -169,27 +171,45 @@ public virtual ShippingByTotalRecord FindShippingByTotalRecord(int shippingMetho
var matchedByZip = new List<ShippingByTotalRecord>();
foreach (var sbt in matchedByStateProvince)
{
if ((String.IsNullOrEmpty(zip) && String.IsNullOrEmpty(sbt.Zip)) ||
(zip.Equals(sbt.Zip, StringComparison.InvariantCultureIgnoreCase)))
if ((zip.IsEmpty() && sbt.Zip.IsEmpty()) || (ZipMatches(zip, sbt.Zip)))
{
matchedByZip.Add(sbt);
}
}

if (matchedByZip.Count == 0)
{
foreach (var taxRate in matchedByStateProvince)
{
if (String.IsNullOrWhiteSpace(taxRate.Zip))
{
matchedByZip.Add(taxRate);
}
}
}
//// Obsolete
//if (matchedByZip.Count == 0)
//{
// foreach (var sbt in matchedByStateProvince)
// {
// if (sbt.Zip.IsEmpty())
// {
// matchedByZip.Add(sbt);
// }
// }
//}

return matchedByZip.FirstOrDefault();
}

private bool ZipMatches(string zip, string pattern)
{
if (pattern.IsEmpty() || pattern == "*")
{
return true; // catch all
}

try
{
var wildcard = new Wildcard(pattern);
return wildcard.IsMatch(zip);
}
catch
{
return zip.IsCaseInsensitiveEqual(pattern);
}
}

/// <summary>
/// Deletes the ShippingByTotalRecord
/// </summary>
Expand Down

0 comments on commit 77cadc2

Please sign in to comment.