TradeRequest fields improperly set when backtesting in MT5 - c++

I'm generating a trade request that's causing a 10013 error when I do backtesting in MT5. I decided to log the trade request and it printed this:
2022.11.16 14:28:17.245 Core 01 2022.01.04 11:00:00 Action REMOVE, Symbol EURUSD, Volume 0.100000, Price 1.128980, Deviation 5, Type SELL STOP LIMIT, Position 9443944
My code is:
bool Strategy::DoEntryRules() {
// Iterate over all the entry rules we have registered...
for (int i = 0; i < m_entry_rules.Count(); i++) {
IEntryRule *rule;
m_entry_rules.TryGetValue(i, rule);
EntryResult result = rule.Enter();
if (result.Result == ENTRY_RESULT_CONTINUE) {
continue;
} else if (result.Result == ENTRY_RESULT_ERROR) {
PrintFormat("Entry rule %s failed with error code %d\n", rule.Name(), result.ErrorCode);
}
m_money_manager.AssignLot(result.Order);
uint errCode = SendOrder(result.Order, rule.Name());
if (errCode != 0) {
result.Result = ENTRY_RESULT_ERROR;
result.ErrorCode = errCode;
}
return true;
}
return false;
}
uint Strategy::SendOrder(const MqlTradeRequest &request, const string name) {
string action;
switch (request.action) {
case TRADE_ACTION_DEAL:
action = "DEAL";
case TRADE_ACTION_SLTP:
action = "SLTP";
case TRADE_ACTION_CLOSE_BY:
action = "CLOSE BY";
case TRADE_ACTION_MODIFY:
action = "MODIFY";
case TRADE_ACTION_PENDING:
action = "PENDING";
case TRADE_ACTION_REMOVE:
action = "REMOVE";
}
string type;
switch (request.type) {
case ORDER_TYPE_BUY:
type = "BUY";
case ORDER_TYPE_BUY_LIMIT:
type = "BUY LIMIT";
case ORDER_TYPE_BUY_STOP:
type = "BUY STOP";
case ORDER_TYPE_BUY_STOP_LIMIT:
type = "BUY STOP LIMIT";
case ORDER_TYPE_CLOSE_BY:
type = "CLOSE BY";
case ORDER_TYPE_SELL:
type = "SELL";
case ORDER_TYPE_SELL_LIMIT:
type = "SELL LIMIT";
case ORDER_TYPE_SELL_STOP:
type = "SELL STOP";
case ORDER_TYPE_SELL_STOP_LIMIT:
type = "SELL STOP LIMIT";
}
PrintFormat("Action %s, Symbol %s, Volume %f, Price %f, Deviation %d, Type %s, Position %d",
action, request.symbol, request.volume, request.price, request.deviation, type, request.position);
MqlTradeCheckResult checkResult;
if (!OrderCheck(request, checkResult)) {
PrintFormat("%s generated a bad order with error code %d: %s", name, checkResult.retcode, checkResult.comment);
return checkResult.retcode;
}
MqlTradeResult result;
if (!OrderSend(request, result) && result.retcode != TRADE_RETCODE_DONE) {
PrintFormat("%s generated an order that was not accepted with error code %d: %s", name, result.retcode, result.comment);
return result.retcode;
}
return 0;
}
where m_entry_rules contains one IEntryRule, which is an interface that has the following implementation:
EntryResult SentimentEntry::Enter() {
EntryResult result;
if (m_handle == 0) {
int errCode = InitATR();
if (errCode != 0) {
result.Result = ENTRY_RESULT_ENTER;
result.ErrorCode = errCode;
return result;
}
}
double values[];
if (CopyBuffer(m_handle, 0, 0, 1, values) < 0) {
result.Result = ENTRY_RESULT_ERROR;
result.ErrorCode = GetLastError();
ResetLastError();
return result;
}
ENUM_SENTIMENT sentiment = SentimentIndicator();
bool decision = GetRange() > 0.66 * values[0];
if (decision && sentiment != SENTIMENT_NEUTRAL) {
PrintFormat("%f > 0.66 * %f, %s", GetRange(), values[0], SentimentToString(sentiment));
result.Result = ENTRY_RESULT_ENTER;
result.Order = CreateOrder(m_magic);
if (sentiment == SENTIMENT_BULLISH) {
result.Order.price = SymbolInfoDouble(Symbol(), SYMBOL_ASK); // ENTERED HERE
result.Order.type = ORDER_TYPE_BUY;
} else if (sentiment == SENTIMENT_BEARISH) {
result.Order.price = SymbolInfoDouble(Symbol(), SYMBOL_BID);
result.Order.type = ORDER_TYPE_SELL;
}
} else {
result.Result = ENTRY_RESULT_CONTINUE;
}
return result;
}
and m_money_manager.AssignLot is defined as:
void ConstantLotManager::AssignLot(MqlTradeRequest &request) {
request.volume = 0.1;
return;
}
Finally, the CreateOrder function is defined as:
MqlTradeRequest CreateOrder(ulong magic) {
MqlTradeRequest request;
request.symbol = Symbol();
request.deviation = 5;
request.action = TRADE_ACTION_DEAL;
request.type_filling = ORDER_FILLING_FOK;
request.type_time = ORDER_TIME_GTC;
request.magic = magic;
return request;
}
From logging, I'm able to determine that the commented line was reached, so the trade request should have an action of TRADE_ACTION_DEAL and a type of ORDER_TYPE_BUY so I'm not sure why it's showing up as me attempting to remove a sell-short order. Does anyone know what's going on here?

I have determined the problem and fixed it. The core issue was that I wasn't converting the ENUM_TRADE_REQUEST_ACTIONS and ENUM_ORDER_TYPE enums to strings properly, giving me the impression of a malformed trade request. The logging statement should look like this:
string action;
switch (request.action) {
case TRADE_ACTION_DEAL:
action = "DEAL";
break;
case TRADE_ACTION_SLTP:
action = "SLTP";
break;
case TRADE_ACTION_CLOSE_BY:
action = "CLOSE BY";
break;
case TRADE_ACTION_MODIFY:
action = "MODIFY";
break;
case TRADE_ACTION_PENDING:
action = "PENDING";
break;
case TRADE_ACTION_REMOVE:
action = "REMOVE";
break;
}
string type;
switch (request.type) {
case ORDER_TYPE_BUY:
type = "BUY";
break;
case ORDER_TYPE_BUY_LIMIT:
type = "BUY LIMIT";
break;
case ORDER_TYPE_BUY_STOP:
type = "BUY STOP";
break;
case ORDER_TYPE_BUY_STOP_LIMIT:
type = "BUY STOP LIMIT";
break;
case ORDER_TYPE_CLOSE_BY:
type = "CLOSE BY";
break;
case ORDER_TYPE_SELL:
type = "SELL";
break;
case ORDER_TYPE_SELL_LIMIT:
type = "SELL LIMIT";
break;
case ORDER_TYPE_SELL_STOP:
type = "SELL STOP";
break;
case ORDER_TYPE_SELL_STOP_LIMIT:
type = "SELL STOP LIMIT";
break;
}
PrintFormat("Action %s, Symbol %s, Volume %f, Price %f, Deviation %d, Type %s, Position %d",
action, request.symbol, request.volume, request.price, request.deviation, type, request.position);
This was my punishment for not remembering that I have to break switch statements in C++. :(

Related

Changing if-else into switch statements?

I am using multiple else/if statements and I want to use switch statements. I tried but don't know how to fill in multiple answers.
How can I convert the following else/if statements to switch?
if (this.state.item.type === 'dashboard') {
settings.view_name = this.state.view_name;
settings.layout = this.state.layout;
settings.inline_edit = this.state.inlineEdit;
settings.show_add = this.state.showAdd;
settings.tab_queries = this.state.tabQueries;
settings.carousel = this.state.carousel;
settings.template = this.state.template;
settings.sort = this.state.sort;
settings.templateOptions = this.state.templateOptions;
} else if (this.state.item.type === 'list_option_percentage_conditions') {
settings.pie_charts = this.state.pie_charts;
} else if (this.state.item.type === 'count_list_options') {
settings.count_list_field = this.state.count_list_field;
settings.date_field = this.state.date_field;
settings.use_creation_date = this.state.use_creation_date;
settings.scheme = this.state.scheme;
} else if (this.state.item.type === 'sum_fields') {
settings.sum_field = this.state.sum_field;
settings.scheme = this.state.scheme;
}
I want it to look something like this:
switch (this.state.iten.type) {
case "dashboard":
answer = "";
break;
case "list_option_percentage_conditions":
answer = "";
case "count_list_options":
answer = "";
break;
case "sum_fields":
answer = "";
break;
}

Capping the value

I have a small script that defines the casting time for all classes in this minor project im working on however i have a few issues.
I want to place a cap on the max value however i'm getting errors!
this is the function i was referring to.
void Player::ApplyRatingMod(CombatRating combatRating, int32 value, bool apply)
{
float oldRating = m_baseRatingValue[combatRating];
m_baseRatingValue[combatRating] += (apply ? value : -value);
// explicit affected values
float const multiplier = GetRatingMultiplier(combatRating);
float const oldVal = oldRating * multiplier;
float const newVal = m_baseRatingValue[combatRating] * multiplier;
switch (combatRating)
{
case CR_HASTE_MELEE:
ApplyAttackTimePercentMod(BASE_ATTACK, oldVal, false);
ApplyAttackTimePercentMod(OFF_ATTACK, oldVal, false);
ApplyAttackTimePercentMod(BASE_ATTACK, newVal, true);
ApplyAttackTimePercentMod(OFF_ATTACK, newVal, true);
break;
case CR_HASTE_RANGED:
ApplyAttackTimePercentMod(RANGED_ATTACK, oldVal, false);
ApplyAttackTimePercentMod(RANGED_ATTACK, newVal, true);
break;
case CR_HASTE_SPELL:
//ApplyCastTimePercentMod(oldVal, false);
//ApplyCastTimePercentMod(newVal, true);
break;
default:
break;
}
UpdateRating(combatRating);
}
void Player::UpdateRating(CombatRating cr)
{
int32 amount = m_baseRatingValue[cr];
// Apply bonus from SPELL_AURA_MOD_RATING_FROM_STAT
// stat used stored in miscValueB for this aura
AuraEffectList const& modRatingFromStat = GetAuraEffectsByType(SPELL_AURA_MOD_RATING_FROM_STAT);
for (AuraEffect const* aurEff : modRatingFromStat)
if (aurEff->GetMiscValue() & (1 << cr))
amount += int32(CalculatePct(GetStat(Stats(aurEff->GetMiscValueB())), aurEff->GetAmount()));
if (amount < 0)
amount = 0;
SetUInt32Value(PLAYER_FIELD_COMBAT_RATING_1 + cr, uint32(amount));
bool affectStats = CanModifyStats();
switch (cr)
{
case CR_WEAPON_SKILL: // Implemented in Unit::RollMeleeOutcomeAgainst
case CR_DEFENSE_SKILL:
UpdateDefenseBonusesMod();
break;
case CR_DODGE:
UpdateDodgePercentage();
break;
case CR_PARRY:
UpdateParryPercentage();
break;
case CR_BLOCK:
UpdateBlockPercentage();
break;
case CR_HIT_MELEE:
UpdateMeleeHitChances();
break;
case CR_HIT_RANGED:
UpdateRangedHitChances();
break;
case CR_HIT_SPELL:
UpdateSpellHitChances();
break;
case CR_CRIT_MELEE:
if (affectStats)
{
UpdateCritPercentage(BASE_ATTACK);
UpdateCritPercentage(OFF_ATTACK);
}
break;
case CR_CRIT_RANGED:
if (affectStats)
UpdateCritPercentage(RANGED_ATTACK);
break;
case CR_CRIT_SPELL:
if (affectStats)
UpdateAllSpellCritChances();
break;
case CR_HIT_TAKEN_MELEE: // Implemented in Unit::MeleeMissChanceCalc
case CR_HIT_TAKEN_RANGED:
break;
case CR_HIT_TAKEN_SPELL: // Implemented in Unit::MagicSpellHitResult
break;
case CR_CRIT_TAKEN_MELEE: // Implemented in Unit::RollMeleeOutcomeAgainst (only for chance to crit)
case CR_CRIT_TAKEN_RANGED:
break;
case CR_CRIT_TAKEN_SPELL: // Implemented in Unit::SpellCriticalBonus (only for chance to crit)
break;
case CR_HASTE_MELEE: // Implemented in Player::ApplyRatingMod
case CR_HASTE_RANGED:
case CR_HASTE_SPELL:
break;
case CR_WEAPON_SKILL_MAINHAND: // Implemented in Unit::RollMeleeOutcomeAgainst
case CR_WEAPON_SKILL_OFFHAND:
case CR_WEAPON_SKILL_RANGED:
break;
case CR_EXPERTISE:
if (affectStats)
{
UpdateExpertise(BASE_ATTACK);
UpdateExpertise(OFF_ATTACK);
}
break;
case CR_ARMOR_PENETRATION:
if (affectStats)
UpdateArmorPenetration(amount);
break;
}
}
void Player::UpdateAllRatings()
{
for (uint8 cr = 0; cr < MAX_COMBAT_RATING; ++cr)
UpdateRating(CombatRating(cr));
}
You can notice how i want to cap the new value outcome to 32000.
Before it gets calculated to percentages!
I tried using
if(newVal > 32000)
newVal = 32000;
Which would normally do the job i guess, but i'm stuck with an error
E0137 expression must be a modifiable lvalue
On the line newVal = 32000;
Remove const qualifier from newVal
float newVal = m_baseRatingValue[combatRating] * multiplier;
You declare newVal as a constant (float const newVal = ...), so you're not allowed to assign a new value.
Removing const should work for you, e.g.
float newVal = m_baseRatingValue[combatRating] * multiplier;
if (newVal > 32000)
newVal = 32000;
...

Refactoring switch or if/else statement? [closed]

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
This question does not appear to be about programming within the scope defined in the help center.
Closed 5 years ago.
Improve this question
i'm working on a school project and got some feedback from my teacher. He said that in my code there are some bad practices, he said that the switch cases could be replaced by a polymorphic approach. Only i have no clue how i could do this.
My code is receiving messages from a CAN bus. Those messages come from different devices, I check the messages from which device they come from. If there is a new device I create a object and parse the message and store the information.
This system is pretty much the same for each message.
Here is my code.
void Application::PollWhisperConnectBus()
{
HAL_GPIO_TogglePin(PORT_LED1, PIN_LED1);
whisper_connect_id_ = hcan2.pRxMsg->StdId;
if (whisper_connect_id_ >= 0x580 && whisper_connect_id_ <= 0x58F)
{
WIBDevice();
}
if (whisper_connect_id_ >= 0x590 && whisper_connect_id_ <= 0x59F)
{
BMSSDevice();
}
if (whisper_connect_id_ >= 0x5B0 && whisper_connect_id_ <= 0x5BF)
{
DCPowerCubeDevice();
}
if (whisper_connect_id_ >= 0x5C0 && whisper_connect_id_ <= 0x5CF)
{
ACPowerCubeDevice();
}
if (whisper_connect_id_ >= 0x700 && whisper_connect_id_ <= 0x70F)
{
WIBHeartBeatDevice();
}
}
This is one of the functions which checked if there is an object of the class, if so parse the message.
void Application::DCPowerCubeDevice()
{
bool found_device = false;
int device = (hcan2.pRxMsg->StdId & 0x0F) + device_instance_offset_;
WhisperConnectDevice* whisper_connect_device;
for(unsigned int i = 0; i < whisper_connect_device_list_.size(); ++i)
{
if ((whisper_connect_device = whisper_connect_device_list_.at(i)) != NULL &&
whisper_connect_device->GetClassName() == "DCPowerCube")
{
DCPowerCube* dc_powercube = dynamic_cast<DCPowerCube*>(whisper_connect_device);
if (dc_powercube != NULL)
{
if (dc_powercube->GetDevice() == device)
{
dc_powercube->ParseCanMessage(&hcan2);
found_device = true;
break;
}
}
}
}
if (!found_device)
{
WhisperConnectDevice* dc_powercube;
if ((dc_powercube = new DCPowerCube) != NULL)
{
dc_powercube->SetDevice(device);
int n2k_address = nmea2000_.FindFirstFreeCanId(n2k_address_, device_list_);
if (n2k_address != 0xFFFF)
{
dc_powercube->SetSrcCanId(n2k_address);
dc_powercube->SetDeviceInstanceOffset(device_instance_offset_);
dc_powercube->SetDeviceInstance(0x30 + device);
dc_powercube->AddressClaim(nmea2000_);
dc_powercube->SendPGN126996(nmea2000_);
dc_powercube->SendPGN126998(nmea2000_, "DCPowerCube", "", "");
device_list_.at(n2k_address) = 0x01;
}
DCPowerCube* dc_powercube2 = dynamic_cast<DCPowerCube*>(dc_powercube);
if (dc_powercube2 != NULL)
{
dc_powercube2->SetCurrentLimit(16);
}
AddToWPCDeviceList(dc_powercube);
}
}
}
void DCPowerCube::ParseCanMessage(CAN_HandleTypeDef *can_handle)
{
if (can_handle != NULL)
{
uint16_t message_index = (can_handle->pRxMsg->Data[1] << 8) + can_handle->pRxMsg->Data[2];
switch (message_index)
{
case 0x1008:
device_name_[0] = can_handle->pRxMsg->Data[4];
device_name_[1] = can_handle->pRxMsg->Data[5];
device_name_[2] = can_handle->pRxMsg->Data[6];
device_name_[3] = can_handle->pRxMsg->Data[7];
device_name_[4] = '\0';
break;
case 0x100A:
software_version_[0] = can_handle->pRxMsg->Data[4];
software_version_[1] = can_handle->pRxMsg->Data[5];
software_version_[2] = can_handle->pRxMsg->Data[6];
software_version_[3] = can_handle->pRxMsg->Data[7];
software_version_[4] = '\0';
break;
case 0x1018:
serial_number_ = can_handle->pRxMsg->Data[4] << 24 | can_handle->pRxMsg->Data[5] << 16 |
can_handle->pRxMsg->Data[6] << 8 | can_handle->pRxMsg->Data[7];
break;
case 0x2100: // DC PowerCube status
power_cube_status_ = can_handle->pRxMsg->Data[4];
io_status_bit_ = can_handle->pRxMsg->Data[5];
dip_switch_status_bit_ = can_handle->pRxMsg->Data[6];
break;
case 0x2111: // Grid voltage, current, current limit
grid_voltage_ = (can_handle->pRxMsg->Data[4] << 8) + can_handle->pRxMsg->Data[5];
grid_current_ = can_handle->pRxMsg->Data[6];
grid_current_limit_ = can_handle->pRxMsg->Data[7];
break;
case 0x2112: // Generator frequency, RPM
generator_freq_ = (can_handle->pRxMsg->Data[4] << 8) + can_handle->pRxMsg->Data[5];
rpm_ = (can_handle->pRxMsg->Data[6] << 8) + can_handle->pRxMsg->Data[7];
break;
case 0x2113: // Generator current
gen_current_phase1_ = can_handle->pRxMsg->Data[4];
gen_current_phase2_ = can_handle->pRxMsg->Data[5];
gen_current_phase3_ = can_handle->pRxMsg->Data[6];
gen_current_limit_ = can_handle->pRxMsg->Data[7];
break;
case 0x2114: // Load percentage
grid_load_ = can_handle->pRxMsg->Data[4];
generator_load_ = can_handle->pRxMsg->Data[5];
dc_output_load_ = can_handle->pRxMsg->Data[6];
break;
case 0x2151: // Battery type & charger state
battery_type_ = can_handle->pRxMsg->Data[4];
charger_state_ = can_handle->pRxMsg->Data[5];
break;
case 0x2152: // DC output voltage & DC slave voltage
dc_output_voltage_ = (can_handle->pRxMsg->Data[4] << 8) + can_handle->pRxMsg->Data[5];
dc_slave_voltage_ = (can_handle->pRxMsg->Data[6] << 8) + can_handle->pRxMsg->Data[7];
break;
case 0x2153: // DC output current & DC output current limit
dc_output_current_ = (can_handle->pRxMsg->Data[4] << 8) + can_handle->pRxMsg->Data[5];
dc_output_current_limit_ = (can_handle->pRxMsg->Data[6] << 8) + can_handle->pRxMsg->Data[7];
break;
case 0x21A0: // Temperature sensor
temp_sens_BTS_ = can_handle->pRxMsg->Data[4];
temp_sens_intern1_ = can_handle->pRxMsg->Data[5];
temp_sens_intern2_ = can_handle->pRxMsg->Data[6];
temp_sens_intern3_ = can_handle->pRxMsg->Data[7];
break;
case 0x21A1:
break;
}
}
}
The WhisperConnectDevice is the base class of DCPowerCube.
I would love to get some feedback on how to approach this problem.
Whether or not you introduce polymorphism it appears you have to map an externally provided type number (ID) to code so you will always need some structure inbetween.
Your candidates are:
A block of if statements probably if-else-if...
A switch statement (if values are ameanable)
Some kind of look-up table (array, associative map, other...)
You've already got if but could improve with if-else-if.
That is normally considered the ugliest high-maintenance potential coding hot-spot approach. Coding hot-spot because all new IDs return to this code block.
I also notice in this case all your ranges are 0xnn0 to 0xnnF inclusive for some nn so you can at least simplify by reducing out the low 4 bits:
auto whisper_connect_type = whisper_connect_id_ >> 4;
Your switch option is then simplified to:
switch(whisper_connect_type) {
case 0x58: WIBDevice(); break;
case 0x59: BMSSDevice(); break;
case 0x5B: DCPowerCubeDevice(); break;
case 0x5C: ACPowerCubeDevice(); break;
case 0x70: WIBHeartBeatDevice(); break;
default: HandleUnknownDeviceIDError(whisper_connect_id_); break;
}
NB: I very strongly recommend some code to handle an unsupported ID. My advice is throwing an exception or something leading to termination. The break; is for completeness. I don't think you're coming back from an unknown ID.
An alternative is to define an associative map:
#include <iostream>
#include <unordered_map>
#include <memory>
class WhisperHandler {
public:
virtual void HandleWhisper() const = 0 ;
virtual ~WhisperHandler() {}
};
class WhisperHandlerWIBDevice : public WhisperHandler {
public:
void HandleWhisper() const override {
std::cout << "Handler WIBDevice...\n";
}
} ;
int main() {
std::unordered_map<unsigned,std::unique_ptr<const WhisperHandler>> handlers;
//...
std::unique_ptr<const WhisperHandler> handler(std::make_unique<const WhisperHandlerWIBDevice>());
std::pair<const unsigned , std::unique_ptr<const WhisperHandler> > pair({0x5B,std::move(handler)});
handlers.insert(std::move(pair));
//...
{
const auto &chandlers=handlers;
auto handlerit(chandlers.find(0x5B1));
if(handlerit!=chandlers.end()){
handlerit->second->HandleWhisper();
}else{
//ERROR - UNKNOWN HANDLER.
}
}
return 0;
}
I would suggest however you're only going to get return on investment for all this polymorphic machinery if you're going to allow the registration of handlers dynamically either from different modules of the application or by dynamically loading libraries that register themselves on load.
If it's a single project application (which it appears to be) then the switch table dispatch will probably work fine.
Because applications tend to communicate using IDs of some kind OO can start to look cumbersome when it in practice it needs to take an ID, map it to a polymorphic handler and then call the handler. Logically you've done the ID to logic mapping twice!
Footnote: The trick of knocking out the lowest 4-bits is somewhat separate from these methods and (of course) slightly fragile if the lower 4 bits become relevant to determining the handler down the line.

Retrieving the status of another user from libpurple (the IM library underpinning Pidgin)

I'm trying to pull the current status of another person on a SIMPLE network (Microsoft Office Communicator). I'm using libpurple, built a c++ wrapper around libpurple, and I can send/receive IMs with other users on the SIMPLE network. What I still need is to get the current status of other users
Here's my current attempt at retrieving status of another user.
Previously defined and initialized:
PurpleAccount *CommonIM::m_account -> I can send messages using this account
// the username of the person I want to get the status of, e.g.
username = "sip:blah#blah.blah.com";
//TEST instance 1
PurpleBuddy* newbody1 = purple_buddy_new(m_account, username.c_str(), NULL);
sleep(5);
PurplePresence *p1 = purple_buddy_get_presence(newbody1);
PurpleStatus *status1 = purple_presence_get_active_status(p1);
PurpleStatusType *statusType1 = purple_status_get_type(status1);
PurpleStatusPrimitive prim1 = purple_status_type_get_primitive(statusType1);
switch(prim1)
{
case PURPLE_STATUS_UNSET:
{
status = "unset";
}
break;
case PURPLE_STATUS_OFFLINE:
{
status = "offline";
}
break;
case PURPLE_STATUS_AVAILABLE:
{
status = "available";
}
break;
case PURPLE_STATUS_UNAVAILABLE:
{
status = "unavailable";
}
break;
case PURPLE_STATUS_INVISIBLE:
{
status = "invisible";
}
break;
case PURPLE_STATUS_AWAY:
{
status = "away";
}
break;
case PURPLE_STATUS_EXTENDED_AWAY:
{
status = "extended away";
}
break;
case PURPLE_STATUS_MOBILE:
{
status = "mobile";
}
break;
case PURPLE_STATUS_TUNE:
{
status = "tune";
}
break;
case PURPLE_STATUS_NUM_PRIMITIVES:
default:
{
status = "unknown";
}
break;
}
//TEST instance 1 complete
cout << _TAG << "Test instance 1: Status for " << username << " is reported as " << status << endl;
This code always returns offline as the status. It's as if purple doesn't refresh the buddy after creating a new instance, it always remains as "offline". I've dived into libpurple and pidgin to try to find this for the past few days but can't find the 'proper' way of retrieving status.
For some reason, calling this from the signed-on signal doesn't work.
Calling it from the buddy-signed-on signal works for me. Of course, in that case it will be called once for each signed-on buddy ...
sample function to be called from the "buddy-signed-on" signal:
static void buddy_signed_on(PurpleBuddy *buddy) {
GSList *buddies = purple_blist_get_buddies();
for(; buddies; buddies = buddies->next) {
PurpleBuddy *b = (PurpleBuddy *) buddies->data;
PurplePresence *presence = purple_buddy_get_presence(b);
PurpleStatus *status = purple_presence_get_active_status(presence);
printf("%s is now %s\n", b->name, purple_status_get_id(status));
}
}
Connect the signal:
purple_signal_connect(purple_blist_get_handle(), "buddy-signed-on", &handle,
PURPLE_CALLBACK(buddy_signed_on), NULL);

How does Stack Overflow generate its SEO-friendly URLs?

What is a good complete regular expression or some other process that would take the title:
How do you change a title to be part of the URL like Stack Overflow?
and turn it into
how-do-you-change-a-title-to-be-part-of-the-url-like-stack-overflow
that is used in the SEO-friendly URLs on Stack Overflow?
The development environment I am using is Ruby on Rails, but if there are some other platform-specific solutions (.NET, PHP, Django), I would love to see those too.
I am sure I (or another reader) will come across the same problem on a different platform down the line.
I am using custom routes, and I mainly want to know how to alter the string to all special characters are removed, it's all lowercase, and all whitespace is replaced.
Here's how we do it. Note that there are probably more edge conditions than you realize at first glance.
This is the second version, unrolled for 5x more performance (and yes, I benchmarked it). I figured I'd optimize it because this function can be called hundreds of times per page.
/// <summary>
/// Produces optional, URL-friendly version of a title, "like-this-one".
/// hand-tuned for speed, reflects performance refactoring contributed
/// by John Gietzen (user otac0n)
/// </summary>
public static string URLFriendly(string title)
{
if (title == null) return "";
const int maxlen = 80;
int len = title.Length;
bool prevdash = false;
var sb = new StringBuilder(len);
char c;
for (int i = 0; i < len; i++)
{
c = title[i];
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
{
sb.Append(c);
prevdash = false;
}
else if (c >= 'A' && c <= 'Z')
{
// tricky way to convert to lowercase
sb.Append((char)(c | 32));
prevdash = false;
}
else if (c == ' ' || c == ',' || c == '.' || c == '/' ||
c == '\\' || c == '-' || c == '_' || c == '=')
{
if (!prevdash && sb.Length > 0)
{
sb.Append('-');
prevdash = true;
}
}
else if ((int)c >= 128)
{
int prevlen = sb.Length;
sb.Append(RemapInternationalCharToAscii(c));
if (prevlen != sb.Length) prevdash = false;
}
if (i == maxlen) break;
}
if (prevdash)
return sb.ToString().Substring(0, sb.Length - 1);
else
return sb.ToString();
}
To see the previous version of the code this replaced (but is functionally equivalent to, and 5x faster), view revision history of this post (click the date link).
Also, the RemapInternationalCharToAscii method source code can be found here.
Here is my version of Jeff's code. I've made the following changes:
The hyphens were appended in such a way that one could be added, and then need removing as it was the last character in the string. That is, we never want “my-slug-”. This means an extra string allocation to remove it on this edge case. I’ve worked around this by delay-hyphening. If you compare my code to Jeff’s the logic for this is easy to follow.
His approach is purely lookup based and missed a lot of characters I found in examples while researching on Stack Overflow. To counter this, I first peform a normalisation pass (AKA collation mentioned in Meta Stack Overflow question Non US-ASCII characters dropped from full (profile) URL), and then ignore any characters outside the acceptable ranges. This works most of the time...
... For when it doesn’t I’ve also had to add a lookup table. As mentioned above, some characters don’t map to a low ASCII value when normalised. Rather than drop these I’ve got a manual list of exceptions that is doubtless full of holes, but it is better than nothing. The normalisation code was inspired by Jon Hanna’s great post in Stack Overflow question How can I remove accents on a string?.
The case conversion is now also optional.
public static class Slug
{
public static string Create(bool toLower, params string[] values)
{
return Create(toLower, String.Join("-", values));
}
/// <summary>
/// Creates a slug.
/// References:
/// http://www.unicode.org/reports/tr15/tr15-34.html
/// https://meta.stackexchange.com/questions/7435/non-us-ascii-characters-dropped-from-full-profile-url/7696#7696
/// https://stackoverflow.com/questions/25259/how-do-you-include-a-webpage-title-as-part-of-a-webpage-url/25486#25486
/// https://stackoverflow.com/questions/3769457/how-can-i-remove-accents-on-a-string
/// </summary>
/// <param name="toLower"></param>
/// <param name="normalised"></param>
/// <returns></returns>
public static string Create(bool toLower, string value)
{
if (value == null)
return "";
var normalised = value.Normalize(NormalizationForm.FormKD);
const int maxlen = 80;
int len = normalised.Length;
bool prevDash = false;
var sb = new StringBuilder(len);
char c;
for (int i = 0; i < len; i++)
{
c = normalised[i];
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
{
if (prevDash)
{
sb.Append('-');
prevDash = false;
}
sb.Append(c);
}
else if (c >= 'A' && c <= 'Z')
{
if (prevDash)
{
sb.Append('-');
prevDash = false;
}
// Tricky way to convert to lowercase
if (toLower)
sb.Append((char)(c | 32));
else
sb.Append(c);
}
else if (c == ' ' || c == ',' || c == '.' || c == '/' || c == '\\' || c == '-' || c == '_' || c == '=')
{
if (!prevDash && sb.Length > 0)
{
prevDash = true;
}
}
else
{
string swap = ConvertEdgeCases(c, toLower);
if (swap != null)
{
if (prevDash)
{
sb.Append('-');
prevDash = false;
}
sb.Append(swap);
}
}
if (sb.Length == maxlen)
break;
}
return sb.ToString();
}
static string ConvertEdgeCases(char c, bool toLower)
{
string swap = null;
switch (c)
{
case 'ı':
swap = "i";
break;
case 'ł':
swap = "l";
break;
case 'Ł':
swap = toLower ? "l" : "L";
break;
case 'đ':
swap = "d";
break;
case 'ß':
swap = "ss";
break;
case 'ø':
swap = "o";
break;
case 'Þ':
swap = "th";
break;
}
return swap;
}
}
For more details, the unit tests, and an explanation of why Facebook's URL scheme is a little smarter than Stack Overflows, I've got an expanded version of this on my blog.
You will want to setup a custom route to point the URL to the controller that will handle it. Since you are using Ruby on Rails, here is an introduction in using their routing engine.
In Ruby, you will need a regular expression like you already know and here is the regular expression to use:
def permalink_for(str)
str.gsub(/[^\w\/]|[!\(\)\.]+/, ' ').strip.downcase.gsub(/\ +/, '-')
end
You can also use this JavaScript function for in-form generation of the slug's (this one is based on/copied from Django):
function makeSlug(urlString, filter) {
// Changes, e.g., "Petty theft" to "petty_theft".
// Remove all these words from the string before URLifying
if(filter) {
removelist = ["a", "an", "as", "at", "before", "but", "by", "for", "from",
"is", "in", "into", "like", "of", "off", "on", "onto", "per",
"since", "than", "the", "this", "that", "to", "up", "via", "het", "de", "een", "en",
"with"];
}
else {
removelist = [];
}
s = urlString;
r = new RegExp('\\b(' + removelist.join('|') + ')\\b', 'gi');
s = s.replace(r, '');
s = s.replace(/[^-\w\s]/g, ''); // Remove unneeded characters
s = s.replace(/^\s+|\s+$/g, ''); // Trim leading/trailing spaces
s = s.replace(/[-\s]+/g, '-'); // Convert spaces to hyphens
s = s.toLowerCase(); // Convert to lowercase
return s; // Trim to first num_chars characters
}
For good measure, here's the PHP function in WordPress that does it... I'd think that WordPress is one of the more popular platforms that uses fancy links.
function sanitize_title_with_dashes($title) {
$title = strip_tags($title);
// Preserve escaped octets.
$title = preg_replace('|%([a-fA-F0-9][a-fA-F0-9])|', '---$1---', $title);
// Remove percent signs that are not part of an octet.
$title = str_replace('%', '', $title);
// Restore octets.
$title = preg_replace('|---([a-fA-F0-9][a-fA-F0-9])---|', '%$1', $title);
$title = remove_accents($title);
if (seems_utf8($title)) {
if (function_exists('mb_strtolower')) {
$title = mb_strtolower($title, 'UTF-8');
}
$title = utf8_uri_encode($title, 200);
}
$title = strtolower($title);
$title = preg_replace('/&.+?;/', '', $title); // kill entities
$title = preg_replace('/[^%a-z0-9 _-]/', '', $title);
$title = preg_replace('/\s+/', '-', $title);
$title = preg_replace('|-+|', '-', $title);
$title = trim($title, '-');
return $title;
}
This function as well as some of the supporting functions can be found in wp-includes/formatting.php.
If you are using Rails edge, you can rely on Inflector.parametrize - here's the example from the documentation:
class Person
def to_param
"#{id}-#{name.parameterize}"
end
end
#person = Person.find(1)
# => #<Person id: 1, name: "Donald E. Knuth">
<%= link_to(#person.name, person_path(#person)) %>
# => Donald E. Knuth
Also if you need to handle more exotic characters such as accents (éphémère) in previous version of Rails, you can use a mixture of PermalinkFu and DiacriticsFu:
DiacriticsFu::escape("éphémère")
=> "ephemere"
DiacriticsFu::escape("räksmörgås")
=> "raksmorgas"
I am not familiar with Ruby on Rails, but the following is (untested) PHP code. You can probably translate this very quickly to Ruby on Rails if you find it useful.
$sURL = "This is a title to convert to URL-format. It has 1 number in it!";
// To lower-case
$sURL = strtolower($sURL);
// Replace all non-word characters with spaces
$sURL = preg_replace("/\W+/", " ", $sURL);
// Remove trailing spaces (so we won't end with a separator)
$sURL = trim($sURL);
// Replace spaces with separators (hyphens)
$sURL = str_replace(" ", "-", $sURL);
echo $sURL;
// outputs: this-is-a-title-to-convert-to-url-format-it-has-1-number-in-it
I hope this helps.
I don't much about Ruby or Rails, but in Perl, this is what I would do:
my $title = "How do you change a title to be part of the url like Stackoverflow?";
my $url = lc $title; # Change to lower case and copy to URL.
$url =~ s/^\s+//g; # Remove leading spaces.
$url =~ s/\s+$//g; # Remove trailing spaces.
$url =~ s/\s+/\-/g; # Change one or more spaces to single hyphen.
$url =~ s/[^\w\-]//g; # Remove any non-word characters.
print "$title\n$url\n";
I just did a quick test and it seems to work. Hopefully this is relatively easy to translate to Ruby.
T-SQL implementation, adapted from dbo.UrlEncode:
CREATE FUNCTION dbo.Slug(#string varchar(1024))
RETURNS varchar(3072)
AS
BEGIN
DECLARE #count int, #c char(1), #i int, #slug varchar(3072)
SET #string = replace(lower(ltrim(rtrim(#string))),' ','-')
SET #count = Len(#string)
SET #i = 1
SET #slug = ''
WHILE (#i <= #count)
BEGIN
SET #c = substring(#string, #i, 1)
IF #c LIKE '[a-z0-9--]'
SET #slug = #slug + #c
SET #i = #i +1
END
RETURN #slug
END
I know it's very old question but since most of the browsers now support unicode urls I found a great solution in XRegex that converts everything except letters (in all languages to '-').
That can be done in several programming languages.
The pattern is \\p{^L}+ and then you just need to use it to replace all non letters to '-'.
Working example in node.js with xregex module.
var text = 'This ! can # have # several $ letters % from different languages such as עברית or Español';
var slugRegEx = XRegExp('((?!\\d)\\p{^L})+', 'g');
var slug = XRegExp.replace(text, slugRegEx, '-').toLowerCase();
console.log(slug) ==> "this-can-have-several-letters-from-different-languages-such-as-עברית-or-español"
Assuming that your model class has a title attribute, you can simply override the to_param method within the model, like this:
def to_param
title.downcase.gsub(/ /, '-')
end
This Railscast episode has all the details. You can also ensure that the title only contains valid characters using this:
validates_format_of :title, :with => /^[a-z0-9-]+$/,
:message => 'can only contain letters, numbers and hyphens'
Brian's code, in Ruby:
title.downcase.strip.gsub(/\ /, '-').gsub(/[^\w\-]/, '')
downcase turns the string to lowercase, strip removes leading and trailing whitespace, the first gsub call globally substitutes spaces with dashes, and the second removes everything that isn't a letter or a dash.
There is a small Ruby on Rails plugin called PermalinkFu, that does this. The escape method does the transformation into a string that is suitable for a URL. Have a look at the code; that method is quite simple.
To remove non-ASCII characters it uses the iconv lib to translate to 'ascii//ignore//translit' from 'utf-8'. Spaces are then turned into dashes, everything is downcased, etc.
You can use the following helper method. It can convert the Unicode characters.
public static string ConvertTextToSlug(string s)
{
StringBuilder sb = new StringBuilder();
bool wasHyphen = true;
foreach (char c in s)
{
if (char.IsLetterOrDigit(c))
{
sb.Append(char.ToLower(c));
wasHyphen = false;
}
else
if (char.IsWhiteSpace(c) && !wasHyphen)
{
sb.Append('-');
wasHyphen = true;
}
}
// Avoid trailing hyphens
if (wasHyphen && sb.Length > 0)
sb.Length--;
return sb.ToString().Replace("--","-");
}
Here's my (slower, but fun to write) version of Jeff's code:
public static string URLFriendly(string title)
{
char? prevRead = null,
prevWritten = null;
var seq =
from c in title
let norm = RemapInternationalCharToAscii(char.ToLowerInvariant(c).ToString())[0]
let keep = char.IsLetterOrDigit(norm)
where prevRead.HasValue || keep
let replaced = keep ? norm
: prevWritten != '-' ? '-'
: (char?)null
where replaced != null
let s = replaced + (prevRead == null ? ""
: norm == '#' && "cf".Contains(prevRead.Value) ? "sharp"
: norm == '+' ? "plus"
: "")
let _ = prevRead = norm
from written in s
let __ = prevWritten = written
select written;
const int maxlen = 80;
return string.Concat(seq.Take(maxlen)).TrimEnd('-');
}
public static string RemapInternationalCharToAscii(string text)
{
var seq = text.Normalize(NormalizationForm.FormD)
.Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark);
return string.Concat(seq).Normalize(NormalizationForm.FormC);
}
My test string:
" I love C#, F#, C++, and... Crème brûlée!!! They see me codin'... they hatin'... tryin' to catch me codin' dirty... "
The stackoverflow solution is great, but modern browser (excluding IE, as usual) now handle nicely utf8 encoding:
So I upgraded the proposed solution:
public static string ToFriendlyUrl(string title, bool useUTF8Encoding = false)
{
...
else if (c >= 128)
{
int prevlen = sb.Length;
if (useUTF8Encoding )
{
sb.Append(HttpUtility.UrlEncode(c.ToString(CultureInfo.InvariantCulture),Encoding.UTF8));
}
else
{
sb.Append(RemapInternationalCharToAscii(c));
}
...
}
Full Code on Pastebin
Edit: Here's the code for RemapInternationalCharToAscii method (that's missing in the pastebin).
I liked the way this is done without using regular expressions, so I ported it to PHP. I just added a function called is_between to check characters:
function is_between($val, $min, $max)
{
$val = (int) $val; $min = (int) $min; $max = (int) $max;
return ($val >= $min && $val <= $max);
}
function international_char_to_ascii($char)
{
if (mb_strpos('àåáâäãåa', $char) !== false)
{
return 'a';
}
if (mb_strpos('èéêëe', $char) !== false)
{
return 'e';
}
if (mb_strpos('ìíîïi', $char) !== false)
{
return 'i';
}
if (mb_strpos('òóôõö', $char) !== false)
{
return 'o';
}
if (mb_strpos('ùúûüuu', $char) !== false)
{
return 'u';
}
if (mb_strpos('çccc', $char) !== false)
{
return 'c';
}
if (mb_strpos('zzž', $char) !== false)
{
return 'z';
}
if (mb_strpos('ssšs', $char) !== false)
{
return 's';
}
if (mb_strpos('ñn', $char) !== false)
{
return 'n';
}
if (mb_strpos('ýÿ', $char) !== false)
{
return 'y';
}
if (mb_strpos('gg', $char) !== false)
{
return 'g';
}
if (mb_strpos('r', $char) !== false)
{
return 'r';
}
if (mb_strpos('l', $char) !== false)
{
return 'l';
}
if (mb_strpos('d', $char) !== false)
{
return 'd';
}
if (mb_strpos('ß', $char) !== false)
{
return 'ss';
}
if (mb_strpos('Þ', $char) !== false)
{
return 'th';
}
if (mb_strpos('h', $char) !== false)
{
return 'h';
}
if (mb_strpos('j', $char) !== false)
{
return 'j';
}
return '';
}
function url_friendly_title($url_title)
{
if (empty($url_title))
{
return '';
}
$url_title = mb_strtolower($url_title);
$url_title_max_length = 80;
$url_title_length = mb_strlen($url_title);
$url_title_friendly = '';
$url_title_dash_added = false;
$url_title_char = '';
for ($i = 0; $i < $url_title_length; $i++)
{
$url_title_char = mb_substr($url_title, $i, 1);
if (strlen($url_title_char) == 2)
{
$url_title_ascii = ord($url_title_char[0]) * 256 + ord($url_title_char[1]) . "\r\n";
}
else
{
$url_title_ascii = ord($url_title_char);
}
if (is_between($url_title_ascii, 97, 122) || is_between($url_title_ascii, 48, 57))
{
$url_title_friendly .= $url_title_char;
$url_title_dash_added = false;
}
elseif(is_between($url_title_ascii, 65, 90))
{
$url_title_friendly .= chr(($url_title_ascii | 32));
$url_title_dash_added = false;
}
elseif($url_title_ascii == 32 || $url_title_ascii == 44 || $url_title_ascii == 46 || $url_title_ascii == 47 || $url_title_ascii == 92 || $url_title_ascii == 45 || $url_title_ascii == 47 || $url_title_ascii == 95 || $url_title_ascii == 61)
{
if (!$url_title_dash_added && mb_strlen($url_title_friendly) > 0)
{
$url_title_friendly .= chr(45);
$url_title_dash_added = true;
}
}
else if ($url_title_ascii >= 128)
{
$url_title_previous_length = mb_strlen($url_title_friendly);
$url_title_friendly .= international_char_to_ascii($url_title_char);
if ($url_title_previous_length != mb_strlen($url_title_friendly))
{
$url_title_dash_added = false;
}
}
if ($i == $url_title_max_length)
{
break;
}
}
if ($url_title_dash_added)
{
return mb_substr($url_title_friendly, 0, -1);
}
else
{
return $url_title_friendly;
}
}
Now all Browser handle nicely utf8 encoding, so you can use WebUtility.UrlEncode Method , its like HttpUtility.UrlEncode used by #giamin but its work outside of a web application.
I ported the code to TypeScript. It can easily be adapted to JavaScript.
I am adding a .contains method to the String prototype, if you're targeting the latest browsers or ES6 you can use .includes instead.
if (!String.prototype.contains) {
String.prototype.contains = function (check) {
return this.indexOf(check, 0) !== -1;
};
}
declare interface String {
contains(check: string): boolean;
}
export function MakeUrlFriendly(title: string) {
if (title == null || title == '')
return '';
const maxlen = 80;
let len = title.length;
let prevdash = false;
let result = '';
let c: string;
let cc: number;
let remapInternationalCharToAscii = function (c: string) {
let s = c.toLowerCase();
if ("àåáâäãåą".contains(s)) {
return "a";
}
else if ("èéêëę".contains(s)) {
return "e";
}
else if ("ìíîïı".contains(s)) {
return "i";
}
else if ("òóôõöøőð".contains(s)) {
return "o";
}
else if ("ùúûüŭů".contains(s)) {
return "u";
}
else if ("çćčĉ".contains(s)) {
return "c";
}
else if ("żźž".contains(s)) {
return "z";
}
else if ("śşšŝ".contains(s)) {
return "s";
}
else if ("ñń".contains(s)) {
return "n";
}
else if ("ýÿ".contains(s)) {
return "y";
}
else if ("ğĝ".contains(s)) {
return "g";
}
else if (c == 'ř') {
return "r";
}
else if (c == 'ł') {
return "l";
}
else if (c == 'đ') {
return "d";
}
else if (c == 'ß') {
return "ss";
}
else if (c == 'Þ') {
return "th";
}
else if (c == 'ĥ') {
return "h";
}
else if (c == 'ĵ') {
return "j";
}
else {
return "";
}
};
for (let i = 0; i < len; i++) {
c = title[i];
cc = c.charCodeAt(0);
if ((cc >= 97 /* a */ && cc <= 122 /* z */) || (cc >= 48 /* 0 */ && cc <= 57 /* 9 */)) {
result += c;
prevdash = false;
}
else if ((cc >= 65 && cc <= 90 /* A - Z */)) {
result += c.toLowerCase();
prevdash = false;
}
else if (c == ' ' || c == ',' || c == '.' || c == '/' || c == '\\' || c == '-' || c == '_' || c == '=') {
if (!prevdash && result.length > 0) {
result += '-';
prevdash = true;
}
}
else if (cc >= 128) {
let prevlen = result.length;
result += remapInternationalCharToAscii(c);
if (prevlen != result.length) prevdash = false;
}
if (i == maxlen) break;
}
if (prevdash)
return result.substring(0, result.length - 1);
else
return result;
}
No, no, no. You are all so very wrong. Except for the diacritics-fu stuff, you're getting there, but what about Asian characters (shame on Ruby developers for not considering their nihonjin brethren).
Firefox and Safari both display non-ASCII characters in the URL, and frankly they look great. It is nice to support links like 'http://somewhere.com/news/read/お前たちはアホじゃないかい'.
So here's some PHP code that'll do it, but I just wrote it and haven't stress tested it.
<?php
function slug($str)
{
$args = func_get_args();
array_filter($args); //remove blanks
$slug = mb_strtolower(implode('-', $args));
$real_slug = '';
$hyphen = '';
foreach(SU::mb_str_split($slug) as $c)
{
if (strlen($c) > 1 && mb_strlen($c)===1)
{
$real_slug .= $hyphen . $c;
$hyphen = '';
}
else
{
switch($c)
{
case '&':
$hyphen = $real_slug ? '-and-' : '';
break;
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
case 'g':
case 'h':
case 'i':
case 'j':
case 'k':
case 'l':
case 'm':
case 'n':
case 'o':
case 'p':
case 'q':
case 'r':
case 's':
case 't':
case 'u':
case 'v':
case 'w':
case 'x':
case 'y':
case 'z':
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
case 'G':
case 'H':
case 'I':
case 'J':
case 'K':
case 'L':
case 'M':
case 'N':
case 'O':
case 'P':
case 'Q':
case 'R':
case 'S':
case 'T':
case 'U':
case 'V':
case 'W':
case 'X':
case 'Y':
case 'Z':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
$real_slug .= $hyphen . $c;
$hyphen = '';
break;
default:
$hyphen = $hyphen ? $hyphen : ($real_slug ? '-' : '');
}
}
}
return $real_slug;
}
Example:
$str = "~!##$%^&*()_+-=[]\{}|;':\",./<>?\n\r\t\x07\x00\x04 コリン ~!##$%^&*()_+-=[]\{}|;':\",./<>?\n\r\t\x07\x00\x04 トーマス ~!##$%^&*()_+-=[]\{}|;':\",./<>?\n\r\t\x07\x00\x04 アーノルド ~!##$%^&*()_+-=[]\{}|;':\",./<>?\n\r\t\x07\x00\x04";
echo slug($str);
Outputs:
コリン-and-トーマス-and-アーノルド
The '-and-' is because &'s get changed to '-and-'.
Rewrite of Jeff's code to be more concise
public static string RemapInternationalCharToAscii(char c)
{
var s = c.ToString().ToLowerInvariant();
var mappings = new Dictionary<string, string>
{
{ "a", "àåáâäãåą" },
{ "c", "çćčĉ" },
{ "d", "đ" },
{ "e", "èéêëę" },
{ "g", "ğĝ" },
{ "h", "ĥ" },
{ "i", "ìíîïı" },
{ "j", "ĵ" },
{ "l", "ł" },
{ "n", "ñń" },
{ "o", "òóôõöøőð" },
{ "r", "ř" },
{ "s", "śşšŝ" },
{ "ss", "ß" },
{ "th", "Þ" },
{ "u", "ùúûüŭů" },
{ "y", "ýÿ" },
{ "z", "żźž" }
};
foreach(var mapping in mappings)
{
if (mapping.Value.Contains(s))
return mapping.Key;
}
return string.Empty;
}