Sunday, November 26, 2017

Real time currency conversion using web service for .Net

Sometimes you need to automatically convert currencies in the app. In order to do that you need to get current conversion ratios for appropriate currencies. There are many web services which provide this data: free and chargeable. In this post I will use free web service provided by Central bank of Russia, in your code you may use other services, e.g. service provided by Central European bank. There is convenient web service which returns currencies conversion ratios for the current day:

http://www.cbr.ru/scripts/XML_daily.asp?date_req=dd/MM/yyyy

E.g. if we want to get it for 26 Nov 2017 we need to use the following url:

http://www.cbr.ru/scripts/XML_daily.asp?date_req=26/11/2017

Here is result:

   1: <?xml version="1.0" encoding="windows-1251"?>
   2: <ValCurs Date="25.11.2017" name="Foreign Currency Market">
   3: <Valute ID="R01010">
   4:     <NumCode>036</NumCode>
   5:     <CharCode>AUD</CharCode>
   6:     <Nominal>1</Nominal>
   7:     <Name>Австралийский доллар</Name>
   8:     <Value>44,5778</Value>
   9: </Valute>
  10: <Valute ID="R01020A">
  11:     <NumCode>944</NumCode>
  12:     <CharCode>AZN</CharCode>
  13:     <Nominal>1</Nominal>
  14:     <Name>Азербайджанский манат</Name>
  15:     <Value>34,4609</Value>
  16: </Valute>
  17: <Valute ID="R01035">
  18:     <NumCode>826</NumCode>
  19:     <CharCode>GBP</CharCode>
  20:     <Nominal>1</Nominal>
  21:     <Name>Фунт стерлингов Соединенного королевства</Name>
  22:     <Value>77,9644</Value>
  23: </Valute>
  24: <Valute ID="R01060">
  25:     <NumCode>051</NumCode>
  26:     <CharCode>AMD</CharCode>
  27:     <Nominal>100</Nominal>
  28:     <Name>Армянских драмов</Name>
  29:     <Value>12,0933</Value>
  30: </Valute>
  31: <Valute ID="R01090B">
  32:     <NumCode>933</NumCode>
  33:     <CharCode>BYN</CharCode>
  34:     <Nominal>1</Nominal>
  35:     <Name>Белорусский рубль</Name>
  36:     <Value>29,2835</Value>
  37: </Valute>
  38: <Valute ID="R01100">
  39:     <NumCode>975</NumCode>
  40:     <CharCode>BGN</CharCode>
  41:     <Nominal>1</Nominal>
  42:     <Name>Болгарский лев</Name>
  43:     <Value>35,4717</Value>
  44: </Valute>
  45: <Valute ID="R01115">
  46:     <NumCode>986</NumCode>
  47:     <CharCode>BRL</CharCode>
  48:     <Nominal>1</Nominal>
  49:     <Name>Бразильский реал</Name>
  50:     <Value>18,1669</Value>
  51: </Valute>
  52: <Valute ID="R01135">
  53:     <NumCode>348</NumCode>
  54:     <CharCode>HUF</CharCode>
  55:     <Nominal>100</Nominal>
  56:     <Name>Венгерских форинтов</Name>
  57:     <Value>22,2428</Value>
  58: </Valute>
  59: <Valute ID="R01200">
  60:     <NumCode>344</NumCode>
  61:     <CharCode>HKD</CharCode>
  62:     <Nominal>10</Nominal>
  63:     <Name>Гонконгских долларов</Name>
  64:     <Value>74,9648</Value>
  65: </Valute>
  66: <Valute ID="R01215">
  67:     <NumCode>208</NumCode>
  68:     <CharCode>DKK</CharCode>
  69:     <Nominal>10</Nominal>
  70:     <Name>Датских крон</Name>
  71:     <Value>93,2302</Value>
  72: </Valute>
  73: <Valute ID="R01235">
  74:     <NumCode>840</NumCode>
  75:     <CharCode>USD</CharCode>
  76:     <Nominal>1</Nominal>
  77:     <Name>Доллар США</Name>
  78:     <Value>58,5318</Value>
  79: </Valute>
  80: <Valute ID="R01239">
  81:     <NumCode>978</NumCode>
  82:     <CharCode>EUR</CharCode>
  83:     <Nominal>1</Nominal>
  84:     <Name>Евро</Name>
  85:     <Value>69,3309</Value>
  86: </Valute>
  87: <Valute ID="R01270">
  88:     <NumCode>356</NumCode>
  89:     <CharCode>INR</CharCode>
  90:     <Nominal>100</Nominal>
  91:     <Name>Индийских рупий</Name>
  92:     <Value>90,5539</Value>
  93: </Valute>
  94: <Valute ID="R01335">
  95:     <NumCode>398</NumCode>
  96:     <CharCode>KZT</CharCode>
  97:     <Nominal>100</Nominal>
  98:     <Name>Казахстанских тенге</Name>
  99:     <Value>17,7114</Value>
 100: </Valute>
 101: <Valute ID="R01350">
 102:     <NumCode>124</NumCode>
 103:     <CharCode>CAD</CharCode>
 104:     <Nominal>1</Nominal>
 105:     <Name>Канадский доллар</Name>
 106:     <Value>45,9975</Value>
 107: </Valute>
 108: <Valute ID="R01370">
 109:     <NumCode>417</NumCode>
 110:     <CharCode>KGS</CharCode>
 111:     <Nominal>100</Nominal>
 112:     <Name>Киргизских сомов</Name>
 113:     <Value>83,9194</Value>
 114: </Valute>
 115: <Valute ID="R01375">
 116:     <NumCode>156</NumCode>
 117:     <CharCode>CNY</CharCode>
 118:     <Nominal>10</Nominal>
 119:     <Name>Китайских юаней</Name>
 120:     <Value>88,6349</Value>
 121: </Valute>
 122: <Valute ID="R01500">
 123:     <NumCode>498</NumCode>
 124:     <CharCode>MDL</CharCode>
 125:     <Nominal>10</Nominal>
 126:     <Name>Молдавских леев</Name>
 127:     <Value>33,7359</Value>
 128: </Valute>
 129: <Valute ID="R01535">
 130:     <NumCode>578</NumCode>
 131:     <CharCode>NOK</CharCode>
 132:     <Nominal>10</Nominal>
 133:     <Name>Норвежских крон</Name>
 134:     <Value>71,9099</Value>
 135: </Valute>
 136: <Valute ID="R01565">
 137:     <NumCode>985</NumCode>
 138:     <CharCode>PLN</CharCode>
 139:     <Nominal>1</Nominal>
 140:     <Name>Польский злотый</Name>
 141:     <Value>16,4902</Value>
 142: </Valute>
 143: <Valute ID="R01585F">
 144:     <NumCode>946</NumCode>
 145:     <CharCode>RON</CharCode>
 146:     <Nominal>1</Nominal>
 147:     <Name>Румынский лей</Name>
 148:     <Value>14,9107</Value>
 149: </Valute>
 150: <Valute ID="R01589">
 151:     <NumCode>960</NumCode>
 152:     <CharCode>XDR</CharCode>
 153:     <Nominal>1</Nominal>
 154:     <Name>СДР (специальные права заимствования)</Name>
 155:     <Value>82,5796</Value>
 156: </Valute>
 157: <Valute ID="R01625">
 158:     <NumCode>702</NumCode>
 159:     <CharCode>SGD</CharCode>
 160:     <Nominal>1</Nominal>
 161:     <Name>Сингапурский доллар</Name>
 162:     <Value>43,4502</Value>
 163: </Valute>
 164: <Valute ID="R01670">
 165:     <NumCode>972</NumCode>
 166:     <CharCode>TJS</CharCode>
 167:     <Nominal>10</Nominal>
 168:     <Name>Таджикских сомони</Name>
 169:     <Value>66,3656</Value>
 170: </Valute>
 171: <Valute ID="R01700J">
 172:     <NumCode>949</NumCode>
 173:     <CharCode>TRY</CharCode>
 174:     <Nominal>1</Nominal>
 175:     <Name>Турецкая лира</Name>
 176:     <Value>14,8728</Value>
 177: </Valute>
 178: <Valute ID="R01710A">
 179:     <NumCode>934</NumCode>
 180:     <CharCode>TMT</CharCode>
 181:     <Nominal>1</Nominal>
 182:     <Name>Новый туркменский манат</Name>
 183:     <Value>16,7243</Value>
 184: </Valute>
 185: <Valute ID="R01717">
 186:     <NumCode>860</NumCode>
 187:     <CharCode>UZS</CharCode>
 188:     <Nominal>10000</Nominal>
 189:     <Name>Узбекских сумов</Name>
 190:     <Value>72,3946</Value>
 191: </Valute>
 192: <Valute ID="R01720">
 193:     <NumCode>980</NumCode>
 194:     <CharCode>UAH</CharCode>
 195:     <Nominal>10</Nominal>
 196:     <Name>Украинских гривен</Name>
 197:     <Value>21,7429</Value>
 198: </Valute>
 199: <Valute ID="R01760">
 200:     <NumCode>203</NumCode>
 201:     <CharCode>CZK</CharCode>
 202:     <Nominal>10</Nominal>
 203:     <Name>Чешских крон</Name>
 204:     <Value>27,2844</Value>
 205: </Valute>
 206: <Valute ID="R01770">
 207:     <NumCode>752</NumCode>
 208:     <CharCode>SEK</CharCode>
 209:     <Nominal>10</Nominal>
 210:     <Name>Шведских крон</Name>
 211:     <Value>70,4829</Value>
 212: </Valute>
 213: <Valute ID="R01775">
 214:     <NumCode>756</NumCode>
 215:     <CharCode>CHF</CharCode>
 216:     <Nominal>1</Nominal>
 217:     <Name>Швейцарский франк</Name>
 218:     <Value>59,6350</Value>
 219: </Valute>
 220: <Valute ID="R01810">
 221:     <NumCode>710</NumCode>
 222:     <CharCode>ZAR</CharCode>
 223:     <Nominal>10</Nominal>
 224:     <Name>Южноафриканских рэндов</Name>
 225:     <Value>41,9715</Value>
 226: </Valute>
 227: <Valute ID="R01815">
 228:     <NumCode>410</NumCode>
 229:     <CharCode>KRW</CharCode>
 230:     <Nominal>1000</Nominal>
 231:     <Name>Вон Республики Корея</Name>
 232:     <Value>53,9061</Value>
 233: </Valute>
 234: <Valute ID="R01820">
 235:     <NumCode>392</NumCode>
 236:     <CharCode>JPY</CharCode>
 237:     <Nominal>100</Nominal>
 238:     <Name>Японских иен</Name>
 239:     <Value>52,5538</Value>
 240: </Valute>
 241: </ValCurs>

It shows conversion ratios related with Russian Ruble, e.g. if we check EUR:

   1: <Valute ID="R01239">
   2:     <NumCode>978</NumCode>
   3:     <CharCode>EUR</CharCode>
   4:     <Nominal>1</Nominal>
   5:     <Name>Евро</Name>
   6:     <Value>69,3309</Value>
   7: </Valute>

it shows that 1 (Nominal) Euro is equal to 69,3309 (Value) Rub. Having conversion ratios for all currencies and Ruble it is possible to get conversion ratios between any 2 currencies using simple proportions math. The following code shows how to convert value between RUR, EUR and USD in any order and direction:

   1:  
   2: public decimal? Convert(decimal sourceValue, Currency sourceCurrency, Currency targetCurrency)
   3: {
   4:     try
   5:     {
   6:         if (sourceCurrency == targetCurrency)
   7:         {
   8:             return sourceValue;
   9:         }
  10:  
  11:         var now = DateTime.Now;
  12:         string url = string.Format("http://www.cbr.ru/scripts/XML_daily.asp?date_req={0}",
  13:             now.ToString("dd/MM/yyyy"));
  14:         string str = this.webClient.Download(url, Encoding.GetEncoding("windows-1251"));
  15:         if (string.IsNullOrEmpty(str))
  16:         {
  17:             return null;
  18:         }
  19:         var doc = XDocument.Parse(str);
  20:         var elEUR = doc.Root.Elements("Valute").FirstOrDefault(e =>
  21:             e.Elements("CharCode").ElementAt(0).Value == "EUR");
  22:         decimal? nominalEUR =
  23:             DecimalHelper.ParseWithDotSeparator(elEUR.Element("Nominal").Value);
  24:         decimal? valueEUR =
  25:             DecimalHelper.ParseWithDotSeparator(elEUR.Element("Value").Value);
  26:  
  27:         var elUSD = doc.Root.Elements("Valute").FirstOrDefault(e =>
  28:             e.Elements("CharCode").ElementAt(0).Value == "USD");
  29:         decimal? nominalUSD =
  30:             DecimalHelper.ParseWithDotSeparator(elUSD.Element("Nominal").Value);
  31:         decimal? valueUSD =
  32:             DecimalHelper.ParseWithDotSeparator(elUSD.Element("Value").Value);
  33:  
  34:         double koefFromEURtoRUB = (double)valueEUR.Value / (double)nominalEUR.Value;
  35:         double koefFromUSDtoRUB = (double)valueUSD.Value / (double)nominalUSD.Value;
  36:  
  37:         double koefFromRUBtoEUR = 1 / koefFromEURtoRUB;
  38:         double koefFromUSDtoEUR = koefFromUSDtoRUB / koefFromEURtoRUB;
  39:  
  40:         double koefFromRUBtoUSD = 1 / koefFromUSDtoRUB;
  41:         double koefFromEURtoUSD = koefFromEURtoRUB / koefFromUSDtoRUB;
  42:  
  43:         double? koeff = null;
  44:         if (sourceCurrency == Currency.Rub)
  45:         {
  46:             if (targetCurrency == Currency.Eur)
  47:             {
  48:                 koeff = koefFromRUBtoEUR;
  49:             }
  50:             else if (targetCurrency == Currency.Usd)
  51:             {
  52:                 koeff = koefFromRUBtoUSD;
  53:             }
  54:         }
  55:         else if (sourceCurrency == Currency.Eur)
  56:         {
  57:             if (targetCurrency == Currency.Rub)
  58:             {
  59:                 koeff = koefFromEURtoRUB;
  60:             }
  61:             else if (targetCurrency == Currency.Usd)
  62:             {
  63:                 koeff = koefFromEURtoUSD;
  64:             }
  65:         }
  66:         else if (sourceCurrency == Currency.Usd)
  67:         {
  68:             if (targetCurrency == Currency.Rub)
  69:             {
  70:                 koeff = koefFromUSDtoRUB;
  71:             }
  72:             else if (targetCurrency == Currency.Eur)
  73:             {
  74:                 koeff = koefFromUSDtoEUR;
  75:             }
  76:         }
  77:         if (koeff == null)
  78:         {
  79:             return null;
  80:         }
  81:  
  82:         var targetValue = (decimal)Math.Round(((double)sourceValue) * koeff.Value, 2);
  83:         return targetValue;
  84:     }
  85:     catch (Exception x)
  86:     {
  87:         return null;
  88:     }
  89: }

Helper classes WebClient and DecimalHelper look like this:

   1: public class WebClient
   2: {
   3:     public string Download(string url, Encoding encoding)
   4:     {
   5:         try
   6:         {
   7:             if (string.IsNullOrEmpty(url))
   8:             {
   9:                 return string.Empty;
  10:             }
  11:             byte[] data;
  12:             using (var webClient = new System.Net.WebClient())
  13:             {
  14:                 data = webClient.DownloadData(url);
  15:             }
  16:             if (data == null)
  17:             {
  18:                 return string.Empty;
  19:             }
  20:             return encoding.GetString(data);
  21:         }
  22:         catch (Exception x)
  23:         {
  24:             return string.Empty;
  25:         }
  26:     }
  27: }

and

   1: public class DecimalHelper
   2: {
   3:     public static decimal? ParseWithDotSeparator(string val)
   4:     {
   5:         if (string.IsNullOrEmpty(val))
   6:         {
   7:             return null;
   8:         }
   9:         return decimal.Parse(val.Replace(",", "."), CultureInfo.InvariantCulture);
  10:     }
  11: }

Convert method receives 3 parameters:

  1. source value
  2. source currency
  3. target currency

At first we download current ratios from web service (lines 10-17). Then we calculate ratios for all required conversions EUR to RUR, USD to RUR, RUR to EUR, USD to EUR, RUR to USD, EUR to USD (lines 35-42) and choice correct ratio depending on arguments. If you will compare result with one get from Google (try to google e.g. eur to usd – Google will show conversion calculator), you will see that it is almost the same. Of course if conversion accuracy is crucial for your app then you need to use another web service which is updated more frequently.

Wednesday, November 15, 2017

Fix problem with “The formula refers to a column that does not exist” error in calculated columns for Sharepoint lists which use owssvr.dll

Sharepoint contains many hidden features which are really useful. One of them is possibility to generate iCal files (.ics) from calendar event. In order to do it you need to add calculated field to the calendar with the following formula:

   1: =”http://<SITE_URL>/_vti_bin/owssvr.dll?CS=109&Cmd=Display&List=<LIST_GUID>
   2: &CacheControl=1&ID=”&ID&”&Using=event.ics”

where instead of <SITE_URL> you should use url of web site where calendar list is located and instead of <LIST_GUID> – guid of the calendar list. You may get list guid if will go to List settings page – guid will be in browser address bar in List query string parameter. Also note that list guid should be added without curly braces because otherwise link won’t be clickable in list item view form.

This formula will work properly on English sites (and for several other languages – see below) but if you will try to use it on some other language site (e.g. on Finnish) you may get the following error:

The formula refers to a column that does not exist. Check the formula for spelling mistakes or change the non-existing column to an existing column

(error message will be translated on the local language as well). The problem is related with internal representation of ID field and with fact that in formulas fields’ display names should be used instead of internal names. If we will check it e.g. using Sharepoint Manager tool there will be property called SchemaXmlWithResourceTokens which contains xml definition of ID field. It will look similar to this:

   1: <Field ID="..."
   2:        ColName="tp_ID"
   3:        RowOrdinal="0"
   4:        ReadOnly="TRUE"
   5:        Type="Counter"
   6:        Name="ID"
   7:        PrimaryKey="TRUE"
   8:        DisplayName="$Resources:core,ID;"
   9:        SourceID="http://schemas.microsoft.com/sharepoint/v3"
  10:        StaticName="ID"
  11:        FromBaseType="TRUE" />

Pay attention of DisplayName attribute of the field: for ID (which is internal field which exists for all list items) it is retrieved from core.resx file, i.e. it is localized as well as other fields. So e.g. for Finnish language (DisplayName=Tunnus) formula will look like this:

   1: =”http://<SITE_URL>/_vti_bin/owssvr.dll?CS=109&Cmd=Display&List=<LIST_GUID>
   2: &CacheControl=1&ID=”&Tunnus&”&Using=event.ics”

I checked core.resx for all languages available for Sharepoint 2013 and found the following languages which will have the same problem: Catalan (ca-ES), Bulgarian (bg-BG), Greek (el-GR), Basque (eu-ES), Hebrew (he-IL), Hungarian (hu-HU), Dutch (nl-nl) uses “Id” instead of “ID”, Polish (pl-PL), Russian (ru-RU), Turkish (tr-TR), Ukranian (uk-UA), Chinese Taiwan (zh-TW). Other languages use “ID” display name for ID field.

Saturday, November 4, 2017

Problem with reusing taxonomy terms with navigation settings in different term sets in Sharepoint

Suppose that we have the following sub sites in the same Sharepoint Online site collection:

  • /en – publishing site
  • /content/en –authoring site

And we need to use the same managed metadata navigation on these publishing and authoring sub sites. In order to achieve that we need to create 2 navigation term sets (e.g. Navigation.en-US and NavigationContent.en-US) and configure navigation settings for the terms:

We can’t use same term set for 2 sub sites because Sharepoint allows to use navigation term set for single site only. As we need to create 2 term sets anyway we would like at least to reuse terms from 1st term set Navigation.en-US in 2nd term set NavigationContent.en-US like it is shown on the picture above (in order to have less maintenance work). And here we face with the problem: it seems like that during reusing navigation settings of the terms are not reused. I.e. if we create terms in 1st term set Navigation.en-US, then reuse them in 2nd term set NavigationContent.en-US and then configure navigation settings in Navigation.en-US – reused terms in NavigationContent.en-US won’t inherit changed navigation settings automatically as we would expect.

Workaround for this problem is quite simple: at first configure navigation settings in source term set (Navigation.en-US) and only after that reuse terms in 2nd term set (NavigationContent.en-US). In this case navigation settings will be inherited properly. But if you will change navigation settings after that in original terms from the source term set – they won’t be changed automatically in 2nd term set. I.e. you will need to change them in 2nd term set explicitly.